From e3c70510a2d6c1771f1ed720d9cba760c8be1af0 Mon Sep 17 00:00:00 2001 From: Adarsha Balaji Date: Tue, 13 Oct 2026 18:29:06 +0000 Subject: [PATCH 1/8] bioreason: port BioReason demo edits from BaseMM_PRISM (baseline, unselected) Squashed diff of BaseMM_PRISM 00c641a..e992faf (bioreason_sft_no_projector_stage). Uncommitted notebook/narration/diagnostic files and the W&B launcher change are not included. Co-Authored-By: Claude Sonnet 5.5 --- CONTRIBUTING.md | 2 +- applications/bioreason/bioreason_demo.ipynb | 3098 +++++++++++++++++ applications/bioreason/eval_kegg_aurora.py | 344 ++ applications/bioreason/flow-diag.png | Bin 0 -> 276630 bytes .../bioreason/helpers/demo_helpers.py | 506 +++ .../download_bioreason_dna_datasets.py | 8 +- .../bioreason/helpers/kernel_wrapper.sh | 28 + .../bioreason/helpers/setup_jupyter_kernel.sh | 64 + .../bioreason/helpers/summarize_eval_sweep.py | 37 + .../{ => scripts/eval}/submit_eval_kegg.sh | 0 .../scripts/eval/submit_eval_kegg_aurora.sh | 94 + .../scripts/eval/submit_eval_sft_sweep.sh | 47 + .../test}/verify_dna_dataset_mix.sh | 4 +- .../training}/submit_bioreason_grpo.sh | 0 .../training/submit_bioreason_grpo_aurora.sh | 141 + .../training/submit_bioreason_projector.sh | 177 + .../scripts/training/submit_bioreason_sft.sh | 174 + .../bioreason/submit_bioreason_projector.sh | 117 - .../bioreason/submit_bioreason_sft.sh | 106 - docs/applications/bioreason-eval.md | 2 +- docs/applications/bioreason_grpo.md | 6 +- src/conf/data/bioreason_demo_mix.yaml | 19 + src/conf/data/bioreason_kegg_only.yaml | 16 + src/conf/model/prism_olmo_1b.yaml | 16 + src/conf/training/bioreason_grpo.yaml | 6 + src/conf/training/bioreason_projector.yaml | 10 +- src/conf/training/bioreason_sft.yaml | 68 +- src/config.py | 4 + src/data/datasets_config.json | 8 +- src/data/multimodal.py | 88 +- src/train.py | 345 +- src/training/distributed.py | 4 +- src/training/trainer_zone_a.py | 82 +- tools/ci/site_path_allowlist.json | 10 +- 34 files changed, 5172 insertions(+), 459 deletions(-) create mode 100644 applications/bioreason/bioreason_demo.ipynb create mode 100644 applications/bioreason/eval_kegg_aurora.py create mode 100644 applications/bioreason/flow-diag.png create mode 100644 applications/bioreason/helpers/demo_helpers.py rename applications/bioreason/{ => helpers}/download_bioreason_dna_datasets.py (78%) create mode 100755 applications/bioreason/helpers/kernel_wrapper.sh create mode 100755 applications/bioreason/helpers/setup_jupyter_kernel.sh create mode 100644 applications/bioreason/helpers/summarize_eval_sweep.py rename applications/bioreason/{ => scripts/eval}/submit_eval_kegg.sh (100%) create mode 100755 applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh create mode 100755 applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh rename applications/bioreason/{ => scripts/test}/verify_dna_dataset_mix.sh (96%) rename applications/bioreason/{ => scripts/training}/submit_bioreason_grpo.sh (100%) create mode 100755 applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh create mode 100755 applications/bioreason/scripts/training/submit_bioreason_projector.sh create mode 100644 applications/bioreason/scripts/training/submit_bioreason_sft.sh delete mode 100755 applications/bioreason/submit_bioreason_projector.sh delete mode 100644 applications/bioreason/submit_bioreason_sft.sh create mode 100644 src/conf/data/bioreason_demo_mix.yaml create mode 100644 src/conf/data/bioreason_kegg_only.yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5fa993a..a5fd0d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,7 +114,7 @@ script for a new application appears, not for the first. The test is what the script *serves*, not what it mentions. `tools/universal_evaluator.py` names KEGG and DOCCI but evaluates every -benchmark, so it is general. `applications/bioreason/submit_bioreason_sft.sh` +benchmark, so it is general. `applications/bioreason/scripts/training/submit_bioreason_sft.sh` runs one training job for one application, so it is not. Note `tools/` and `scripts/` are not cleanly separated from each other — diff --git a/applications/bioreason/bioreason_demo.ipynb b/applications/bioreason/bioreason_demo.ipynb new file mode 100644 index 0000000..b472489 --- /dev/null +++ b/applications/bioreason/bioreason_demo.ipynb @@ -0,0 +1,3098 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "arch-diagram", + "metadata": {}, + "source": [ + "# PRISM-BioReason Model Architecture\n", + "\n", + "![PRISM-BioReason model architecture](flow-diag.png)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-000", + "metadata": {}, + "source": [ + "# BioReason on Aurora: training a DNA + language model with PRISM\n", + "\n", + "This notebook trains **BioReason-style DNA reasoning** with PRISM: an LLM reads a reference and a variant DNA sequence, then answers *\"which disease does this variant contribute to?\"*. It runs on **one Aurora interactive node** (12 PVC tiles).\n", + "\n", + "Key stages of training and respective files:\n", + "| Stage | Config (`src/conf/training/`) | Trains | Frozen | Script |\n", + "|---|---|---|---|---|\n", + "| 1. Projector alignment | `bioreason_projector.yaml` | DNA projector | NT encoder, OLMo | `scripts/training/submit_bioreason_projector.sh` |\n", + "| 2. SFT | `bioreason_sft.yaml` | projector + LoRA (r=32) | NT encoder, OLMo base | `scripts/training/submit_bioreason_sft.sh` |\n", + "| 3. GRPO | `bioreason_grpo.yaml` | LoRA (r=16, on top of merged SFT LoRA) | everything else | `scripts/training/submit_bioreason_grpo_aurora.sh` |\n", + "| 4. Eval | — | — | — | `scripts/eval/submit_eval_kegg_aurora.sh` |\n", + "\n", + "**Sections**\n", + "0. Environment installation (terminal, once)\n", + "1. Settings and environment variables (HF cache)\n", + "2. Prerequisites\n", + "3. Download models and data from Hugging Face\n", + "4. Visualize the data\n", + "5. `datasets_config.json`: where datasets are registered\n", + "6. How the DNA modality is registered\n", + "7. Pointing at the DNA encoder\n", + "8. Special tokens\n", + "9. Model build: inspect, then modify the projector and LoRA\n", + "10. One batch end to end (tokenization view and forward pass)\n", + "11. Local smoke run on this node\n", + "12. Submitting all four stages with `qsub`\n", + "13. Monitoring and results\n", + "14. Troubleshooting\n", + "\n", + "> **Before you start:** follow section 0 to build the environment and connect this notebook to the **PRISM BioReason (Aurora)** kernel." + ] + }, + { + "cell_type": "markdown", + "id": "cell-env-install", + "metadata": {}, + "source": [ + "## Environment installation (once, in a terminal, before opening this notebook)\n", + "\n", + "The notebook's kernel *is* this environment, so build it from a shell first. It follows the README's Aurora rules (`uv`-managed venv on `/flare`, PRISM installed with `--no-deps` so pip never replaces the module's XPU PyTorch, frameworks module loaded **before** the venv), with one difference noted at the end.\n", + "\n", + "**① On a login node** (it has direct internet access):\n", + "```bash\n", + "# uv (README: required by the Aurora env builders)\n", + "curl -LsSf https://astral.sh/uv/install.sh | sh\n", + "\n", + "# code\n", + "git clone https://github.com/AI-ModCon/BaseMM_PRISM.git\n", + "cd BaseMM_PRISM && git checkout bioreason_tutorial\n", + "\n", + "# venv + Jupyter kernel: loads frameworks/2026.1.0, creates a --system-site-packages\n", + "# venv at /flare/ModCon/$USER/prism-envs/bioreason-fw2026.1, installs the extras the module\n", + "# lacks (torchtune, webdataset, wandb, ipykernel, ipywidgets, jupyter_server) with --no-deps,\n", + "# installs PRISM itself with --no-deps, and registers the \"PRISM BioReason (Aurora)\" kernel.\n", + "bash applications/bioreason/helpers/setup_jupyter_kernel.sh\n", + "\n", + "# on the compute node\n", + "module use /soft/modulefiles && module load frameworks/2026.1.0 # module first ...\n", + "\n", + "python -m venv .venv && source .venv/bin/activate\n", + "pip install -r requirements/base.txt\n", + "pip install -e . --no-deps\n", + "\n", + "source /flare/ModCon/$USER/$PROJECT/BaseMM_PRISM/.venv/bin/activate # ... then the venv\n", + "cd /flare/ModCon/$USER/$PROJECT/BaseMM_PRISM # wherever you cloned it" + ] + }, + { + "cell_type": "markdown", + "id": "fd6c415d", + "metadata": {}, + "source": [ + "# Connect Jupyter notebook to interactive compute node. \n", + "\n", + "** Get a compute node and start a Jupyter server on it:**\n", + "```\n", + "\n", + "```bash\n", + "\n", + "qsub -I -l select=1 -l walltime=1:00:00 -q debug -A ModCon -l filesystems=home:flare\n", + "\n", + "source /flare/ModCon/$USER/$PROJECT/BaseMM_PRISM/.venv/bin/activate # ... then the venv\n", + "\n", + "jupyter server --no-browser --ip=$(hostname -f) --port 8888 # prints the URL + a new random token\n", + "\n", + "```\n", + "\n", + "Lost the Jupyter URL? \n", + "Run `jupyter server list` on the node. \n", + "\n", + "To reuse **one fixed token** across nodes, create it once with \n", + "`python3 -c \"import secrets; print(secrets.token_hex(24))\" > ~/.jupyter_token && chmod 600 ~/.jupyter_token`, then start the server with:\n", + "\n", + "```bash\n", + "test -s ~/.jupyter_token && jupyter server --no-browser --ip=$(hostname -f) --port 8888 \\\n", + " --IdentityProvider.token=\"$(cat ~/.jupyter_token)\"\n", + "```\n", + "\n", + "**③ Connect VS Code** (Remote-SSH to the login node):\n", + "\n", + "1. **Select Kernel → Select Another Kernel… → Existing Jupyter Server**, paste `http://.hostmgmt.cm.aurora.alcf.anl.gov:8888/?token=`. Use the **full** hostname: it matches `no_proxy=*.alcf.anl.gov`, so the request isn't routed through the ALCF proxy (`502 Bad Gateway`).\n", + "2. Pick **PRISM BioReason (Aurora)**, not \"Python 3\".\n", + "3. If it hangs on \"Connecting to kernel\", set `\"http.proxySupport\": \"off\"` in **Remote Settings** and run **Developer: Reload Window**. `403 Forbidden` means a stale token: each server start makes a new one unless you use a fixed token.\n", + "\n", + "**④ Optional site paths.** The README's `PRISM_*` variables (`cp .env.template .env`, Aurora values in `.env.aurora.example`) are **not** needed here: section 1 sets everything this notebook and its jobs use.\n", + "\n", + "> **Why not `tools/build_aurora_env.sh`?** The README's canonical Aurora build pins `frameworks/2025.3.1` and its lockfile. The BioReason submit scripts and this tutorial run on **`frameworks/2026.1.0`**, the stack everything here was tested on, which is why they use their own venv. Don't mix the two: a venv built against one frameworks module and loaded under the other fails with import or ABI errors. Note that 2026.1.0 ships transformers 5.x, so only Nucleotide Transformer **v1** checkpoints (e.g. the 2.5B multi-species one used here) load. NT-v2's remote code doesn't.\n", + "\n", + "Section 2 below verifies the result (12 XPU tiles, venv active, no missing packages)." + ] + }, + { + "cell_type": "markdown", + "id": "cell-001", + "metadata": {}, + "source": [ + "## 1. Settings and environment variables\n", + "\n", + "Everything site-specific is set here and nowhere else. Later sections pass the same values to batch jobs through `qsub -v`.\n", + "\n", + "| Variable | Why |\n", + "|---|---|\n", + "| `HF_HOME` | Hugging Face cache for **models and datasets**. It must be on `/flare`: home has a small quota, and an earlier projector job died with `Disk quota exceeded` at its first checkpoint. The submit scripts look for `$HF_HOME/hub/models--…`. |\n", + "| `PRISM_DATA_ROOT` | `datasets_config.json` resolves `${PRISM_DATA_ROOT}/dna_local_shards/kegg` from it. |\n", + "| `PRISM_CKPT_ROOT` | Checkpoints (~13 GB each): every run writes to `$PRISM_CKPT_ROOT//` (`hydra.run.dir`). |\n", + "| `PRISM_OUTPUT_ROOT` | Logs, eval results, and generated scripts. |\n", + "| `HTTP(S)_PROXY` | Aurora compute nodes reach the internet only through the ALCF proxy. |\n", + "| `HF_HUB_OFFLINE` | Off while downloading (section 3), on for training, which is what the batch jobs do. |\n", + "| `MAX_SEQ_LENGTH` | Cap on the **merged** sequence (text + 2 × 1024 DNA slots ≈ 2.8–3.6k tokens for KEGG). The 2048 default makes the merged-length guard reject every DNA example (section 10). |" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cell-002", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "repo /home/abalaji/projects/modcon/release/BaseMM_PRISM\n", + "run tag demo-20261011-2324\n", + "HF_HOME /flare/ModCon/abalaji/cache/model_weights\n", + "PRISM_DATA_ROOT /lus/flare/projects/ModCon/abalaji/cache\n", + "PRISM_CKPT_ROOT /lus/flare/projects/ModCon/abalaji/modcon/checkpoints\n", + "PRISM_OUTPUT_ROOT /lus/flare/projects/ModCon/abalaji/output\n" + ] + } + ], + "source": [ + "import os, sys, getpass, datetime\n", + "from pathlib import Path\n", + "\n", + "USER = getpass.getuser()\n", + "PROJECT = \"ModCon\" # PBS allocation (-A)\n", + "\n", + "REPO = Path.cwd().resolve().parents[1] if Path.cwd().name == \"bioreason\" else Path.cwd().resolve()\n", + "PRISM_DATA_ROOT = f\"/lus/flare/projects/{PROJECT}/{USER}/cache\"\n", + "PRISM_OUTPUT_ROOT = f\"/lus/flare/projects/{PROJECT}/{USER}/output\"\n", + "PRISM_CKPT_ROOT = f\"/lus/flare/projects/{PROJECT}/{USER}/modcon/checkpoints\" # = the submit scripts' default\n", + "HF_HOME = f\"/flare/{PROJECT}/{USER}/cache/model_weights\" # = the submit scripts' HF_HOME\n", + "PRISM_VENV = os.environ.get(\"PRISM_VENV\", f\"/flare/{PROJECT}/{USER}/prism-envs/bioreason-fw2026.1\")\n", + "PROXY = \"http://proxy.alcf.anl.gov:3128\"\n", + "\n", + "RUN_TAG = datetime.datetime.now().strftime(\"demo-%Y%m%d-%H%M\") # one tag for all runs from this session\n", + "SUBMIT = False # flip to True in section 12 to actually qsub\n", + "\n", + "ENV = {\n", + " \"PRISM_DATA_ROOT\": PRISM_DATA_ROOT,\n", + " \"PRISM_OUTPUT_ROOT\": PRISM_OUTPUT_ROOT,\n", + " \"PRISM_CKPT_ROOT\": PRISM_CKPT_ROOT,\n", + " \"PRISM_VENV\": PRISM_VENV,\n", + " \"HF_HOME\": HF_HOME,\n", + " \"HF_HUB_CACHE\": f\"{HF_HOME}/hub\",\n", + " \"HTTP_PROXY\": PROXY, \"HTTPS_PROXY\": PROXY, \"http_proxy\": PROXY, \"https_proxy\": PROXY,\n", + " \"no_proxy\": \"localhost,127.0.0.1,.alcf.anl.gov\",\n", + " \"WANDB_MODE\": \"offline\",\n", + " \"MAX_SEQ_LENGTH\": \"4096\",\n", + " \"TOKENIZERS_PARALLELISM\": \"false\",\n", + "}\n", + "os.environ.update(ENV)\n", + "for d in (PRISM_DATA_ROOT, PRISM_OUTPUT_ROOT, PRISM_CKPT_ROOT, HF_HOME):\n", + " Path(d).mkdir(parents=True, exist_ok=True)\n", + "\n", + "os.chdir(REPO) # model YAMLs use repo-relative tokenizer paths\n", + "sys.path.insert(0, str(REPO))\n", + "sys.path.insert(0, str(REPO / \"applications\" / \"bioreason\"))\n", + "sys.path.insert(0, str(REPO / \"applications\" / \"bioreason\" / \"helpers\"))\n", + "import demo_helpers as H # applications/bioreason/helpers/demo_helpers.py\n", + "\n", + "print(\"repo \", REPO)\n", + "print(\"run tag\", RUN_TAG)\n", + "for k in (\"HF_HOME\", \"PRISM_DATA_ROOT\", \"PRISM_CKPT_ROOT\", \"PRISM_OUTPUT_ROOT\"):\n", + " print(f\"{k:18s}\", ENV[k])" + ] + }, + { + "cell_type": "markdown", + "id": "cell-003", + "metadata": {}, + "source": [ + "## 2. Prerequisites\n", + "\n", + "PRISM on Aurora uses the **`frameworks/2026.1.0`** module (XPU PyTorch, oneCCL) plus a thin `--system-site-packages` venv holding only what the module lacks: `torchtune` for LoRA, `webdataset`, `wandb`, and `ipywidgets` for this notebook. Extras are installed with `--no-deps` so pip never swaps the module's XPU torch for a CUDA wheel.\n", + "\n", + "> Do **not** activate the repo's `.venv`. It carries a CUDA torch build, and its transformers 5.x can't run the Nucleotide Transformer **v2** remote code (see section 7)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cell-004", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "python /flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1/bin/python\n", + "in venv True (sys.prefix=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1)\n", + "torch 2.13.0a0+gitcf30153 | transformers 5.14.1\n", + "XPU tiles 12 (expect 12 on an Aurora node)\n", + "missing none\n" + ] + } + ], + "source": [ + "import importlib, torch, transformers\n", + "\n", + "print(\"python \", sys.executable)\n", + "print(\"in venv \", Path(sys.prefix).resolve() == Path(PRISM_VENV).resolve(), f\"(sys.prefix={sys.prefix})\")\n", + "print(\"torch \", torch.__version__, \"| transformers\", transformers.__version__)\n", + "print(\"XPU tiles \", torch.xpu.device_count() if hasattr(torch, \"xpu\") else 0, \"(expect 12 on an Aurora node)\")\n", + "missing = [m for m in (\"torchtune\", \"webdataset\", \"ipywidgets\", \"hydra\", \"datasets\", \"wandb\")\n", + " if importlib.util.find_spec(m) is None]\n", + "print(\"missing \", missing or \"none\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cell-005", + "metadata": {}, + "outputs": [], + "source": [ + "# Only needed if something is listed as missing above (same recipe as the submit scripts' header).\n", + "if missing:\n", + " !uv pip install wandb braceexpand==0.1.7 ipywidgets\n", + " !uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1\n", + " !uv pip install --no-deps -e {REPO}\n", + " print(\"Installed - restart the kernel and re-run section 1.\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-006", + "metadata": {}, + "source": [ + "## 3. Download models and data from Hugging Face\n", + "\n", + "**Models.** `snapshot_download` is called *without* `cache_dir`, so files land under `$HF_HOME/hub/models----`. That's the layout `transformers` resolves offline, and the layout the submit scripts' pre-flight checks look for.\n", + "\n", + "| Model | Role | Size |\n", + "|---|---|---|\n", + "| `allenai/OLMo-1B-0724-hf` | LLM backbone (d_model 2048) | ~4.7 GB |\n", + "| `InstaDeepAI/nucleotide-transformer-2.5b-multi-species` | DNA encoder **and** DNA tokenizer (6-mers) | ~10 GB |" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cell-007", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "llm: allenai/OLMo-1B-0724-hf\n", + " -> /flare/ModCon/abalaji/cache/model_weights/hub/models--allenai--OLMo-1B-0724-hf/snapshots/d7cbab742d80589e714b1a2d7f838dcd21cbe143\n", + "dna: InstaDeepAI/nucleotide-transformer-2.5b-multi-species\n", + " -> /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n" + ] + } + ], + "source": [ + "os.environ[\"HF_HUB_OFFLINE\"] = \"0\"\n", + "from huggingface_hub import snapshot_download\n", + "\n", + "MODELS = {\n", + " \"llm\": \"allenai/OLMo-1B-0724-hf\",\n", + " \"dna\": \"InstaDeepAI/nucleotide-transformer-2.5b-multi-species\",\n", + "}\n", + "SNAPSHOTS = {}\n", + "for role, repo_id in MODELS.items():\n", + " local = Path(HF_HOME, \"hub\", \"models--\" + repo_id.replace(\"/\", \"--\"))\n", + " SNAPSHOTS[role] = snapshot_download(repo_id, local_files_only=local.exists())\n", + " print(f\"{role}: {repo_id}\\n -> {SNAPSHOTS[role]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-008", + "metadata": {}, + "source": [ + "**Data.** \n", + "\n", + "`helpers/download_bioreason_dna_datasets.py` saves each Hugging Face dataset with `save_to_disk` (Arrow) under `//`. \n", + "\n", + "PRISM's streaming loader globs `*.arrow` from the `local_path` given in `datasets_config.json`, so training never touches the Hub.\n", + "\n", + "| Dataset | Rows (train) | Fields |\n", + "|---|---|---|\n", + "| `wanglab/kegg` | 1,159 | `question, answer, reasoning, reference_sequence, variant_sequence` |\n", + "| `wanglab/variant_effect_coding` | 48,850 | `question, answer, reference_sequence, variant_sequence` |\n", + "| `wanglab/variant_effect_non_snv` | 35,215 | `question, answer, reference_sequence, mutated_sequence` |\n", + "\n", + "KEGG's `val` and `test` splits go to separate roots, because the script names the output folder after the dataset, not the split." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cell-009", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "find: warning: you have specified the global option -maxdepth after the argument -name, but global options are not positional, i.e., -maxdepth affects tests specified before it as well as those specified after it. Please specify global options before other arguments.\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg/data-00000-of-00001.arrow\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg/dataset_info.json\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards_test/kegg/data-00000-of-00001.arrow\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards_test/kegg/dataset_info.json\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards_val/kegg/data-00000-of-00001.arrow\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards_val/kegg/dataset_info.json\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_coding/data-00000-of-00001.arrow\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_coding/dataset_info.json\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_non_snv/data-00000-of-00001.arrow\n", + "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_non_snv/dataset_info.json\n" + ] + } + ], + "source": [ + "DL = REPO / \"applications/bioreason/helpers/download_bioreason_dna_datasets.py\"\n", + "SHARDS = Path(PRISM_DATA_ROOT, \"dna_local_shards\")\n", + "\n", + "for key in (\"kegg\", \"variant_effect_coding\", \"variant_effect_non_snv\"):\n", + " if not (SHARDS / key / \"dataset_info.json\").exists():\n", + " !python {DL} --dataset {key} --output-dir {SHARDS}\n", + "for split in (\"val\", \"test\"):\n", + " if not Path(PRISM_DATA_ROOT, f\"dna_local_shards_{split}\", \"kegg\", \"dataset_info.json\").exists():\n", + " !python {DL} --dataset kegg --split {split} --output-dir {PRISM_DATA_ROOT}/dna_local_shards_{split}\n", + "\n", + "os.environ[\"HF_HUB_OFFLINE\"] = \"1\" # from here on, run like the batch jobs: fully offline\n", + "os.environ[\"HF_DATASETS_OFFLINE\"] = \"1\"\n", + "os.environ[\"TRANSFORMERS_OFFLINE\"] = \"1\"\n", + "!find {SHARDS} {PRISM_DATA_ROOT}/dna_local_shards_val {PRISM_DATA_ROOT}/dna_local_shards_test -maxdepth 2 -name \"*.arrow\" -o -maxdepth 2 -name dataset_info.json | sort" + ] + }, + { + "cell_type": "markdown", + "id": "cell-010", + "metadata": {}, + "source": [ + "## 4. Multimodal task the model is being trained for and the respective datasets" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "cell-011", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "kegg 1,159 rows columns=['question', 'answer', 'reasoning', 'reference_sequence', 'variant_sequence']\n", + "variant_effect_coding 48,850 rows columns=['ID', 'question', 'answer', 'reference_sequence', 'variant_sequence']\n", + "variant_effect_non_snv 35,215 rows columns=['question', 'answer', 'reference_sequence', 'mutated_sequence', 'cleaned_pathogenicity', '__index_level_0__']\n", + "kegg (test) 146 rows columns=['question', 'answer', 'reasoning', 'reference_sequence', 'variant_sequence']\n" + ] + }, + { + "data": { + "image/png": "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4eKFXOPZ6d3nQbr36HndsczERF+x6bzi4+Mt+aakpBAfH4+zszOOjo5cuXKFwMBAVq9eTevWrfNs/8Ybb1CpUiXatWtHeno6s2fPJjIykvvvvx+AgwcP8v333/PEE08QFxfH5MmTKVWqFP369bMpdxERKbo0sVws/P0DGPzEEObNncOZM6GW7R4enphMJgYNfpKnho2w/Hv7nfeoVq0aHu4epKamkpKSYtknLi72n/3ziGfH0dERNzc3yz9XVzebz+vaek6JiYlMnDiRypUr8/HHHwNw+vRpfH19qVOnjlXtR44cyd69e+ncuTNdu3YlPDyc1atXU7FiRQBq1aqFn58fXbt2pU+fPpQpU4aff/4ZZ2dnm/MXEZGiSVeiJItu3brj6urK65NeZeLLrxIcXJWyZcsSGFiJ3bt20X/AQOzt7UlNTeXHH3+gYsVAypUvT+nSZVi1aiUPP9yPjIwMfvhhpaXPvOK3W05rOgH8+eefTJgwgRIlSljVvlSpUsyYMSPHuMlkYsyYMYwZM8a2ZEVEpNhQESU36dChIy7Ozrw1+U2ef+FFatWqzXMTXmDevNk8MfhxvL1LEB0dTZcuXXFwcMBkMvH0M2OZOuU91q9bR3p6GnXr1QfA3t4Oe3v7XOOFaezYsYV6fBERKb5URAkA9es3yLJCd4uWrahcpQqpqZlPyPn7+/Pmm28THx9PTEwM/v7+WT4Xrnr1Gnw6/wsiIi7i4+NLaOhptvy52XIbLq+4iIhIcaMiSgDw9PTE09MzyzZ//4Cb2nl4eODh4XHT9r/+OkDp0mUoW7YcKSkpLF/+PXXr1sPOzs6quIiISHGjIkryhY+PD2+99QYYBjExMdx1VwXGj59gdVxERKS4UREl+aJ8+buYNWsuERERuLi44O3tfUtxERGR4kZFlOQbk8mEv7+/zXEREZHiRBNSRERERGygIkpERETEBiqiRERERGygIkpERETEBppYLsXK1teCsyzyKSIiUlh0JUpERETEBiqiRERERGygIkpERETEBiqiRERERGygIkpERETEBiqiRERERGygIkpERETEBiqiRERERGygIkpERETEBiqiRERERGygIkpERETEBiqiRERERGygIkpERETEBiqiRERERGygIkpERETEBiqiRERERGygIkpERETEBiqiRERERGzgUNgJiNyKZm+eIDGtsLMo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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from datasets import load_from_disk\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import HTML, display\n", + "\n", + "DS = {k: load_from_disk(str(SHARDS / k)) for k in (\"kegg\", \"variant_effect_coding\", \"variant_effect_non_snv\")}\n", + "DS[\"kegg (test)\"] = load_from_disk(f\"{PRISM_DATA_ROOT}/dna_local_shards_test/kegg\")\n", + "for k, d in DS.items():\n", + " print(f\"{k:24s} {len(d):>7,} rows columns={d.column_names}\")\n", + "H.plot_counts({k: len(d) for k, d in DS.items()}, \"Rows per dataset\");" + ] + }, + { + "cell_type": "markdown", + "id": "cell-012", + "metadata": {}, + "source": [ + "### Sample browser\n", + "\n", + "Pick a dataset and a row. Bases are colored by letter, and the positions where the variant differs from the reference are highlighted: single-base substitutions in `variant_effect_coding`, insertions and deletions in `variant_effect_non_snv`. Only a window around the first difference is shown." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "cell-013", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "edd3c2487aa64d4b8c57521daa5fa4e7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(Dropdown(description='dataset', options=('kegg', 'variant_effect_coding', 'varia…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.sample_browser(DS)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-014", + "metadata": {}, + "source": [ + "### How long are the sequences, in tokens?\n", + "\n", + "The model doesn't see base pairs. The NT tokenizer turns DNA into **6-mers** (≈ bp / 6), and every DNA span is padded or truncated to exactly `max_dna_length = 1024` tokens. That fixed length is what makes the splice in section 10 work. Text goes through the OLMo tokenizer." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "cell-015", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABpoAAAFKCAYAAAANA4zPAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAA+xtJREFUeJzs3XdUFFcbBvCHXpYmRaqCYAVExC427BULYo0BTUyMGk2iUaNJLFFjjCYxGo0tGgs27CVq7EHF3guoKB2k97Kw8/3Bx8SVBRZEV8PzO4dzmLkzd98Zlml37nvVBEEQQERERERERERERERERFRB6qoOgIiIiIiIiIiIiIiIiN5NbGgiIiIiIiIiIiIiIiKiSmFDExEREREREREREREREVUKG5qIiIiIiIiIiIiIiIioUtjQRERERERERERERERERJXChiYiIiIiIiIiIiIiIiKqFDY0ERERERERERERERERUaWwoYmIiIiIiIiIiIiIiIgqhQ1NREREREREREREREREVCmaqg6AqCIOHDiA5ORkAICWlhaMjY1Rv3591K9fv8zlu3XrBltbW7myLVu2wN3dHa6urnLzpVIpAgICYGlpiZ49e76eDXnB9u3b4ebmBmdn59f+WfT63L17F7GxsejWrZuqQxFdv34dGRkZ6Nixo6pDeecIgoA///wTXl5esLe3f+Of/+DBA9y8eRPDhw9/459dnrt37+L+/fsYMmTIa/uM1NRUHD9+HDKZDMOGDSt1uZiYGJw/fx5aWlpo3749zMzMSiwjlUpx/fp1hIeHw9bWFi1btoSWllaZn3/69GmEh4ejc+fOqF279itvT2UkJSUhODgY6enpsLKyQuvWraGnp6eSWMqSnZ2NnTt3omfPnrCyslJ1OGVKS0vD3r170bdvX5ibm6s6nDKdOnUKZmZmaNKkidx8mUyGa9euISwsDFpaWmjatCnq1KlTYv2LFy8iMjKy1P/TixcvIiQkBPb29vDy8ipRvnv3bmRkZKBVq1Zo1KiR0nEfPXoU9vb2FVqHqLq6cOECQkNDAQDq6uowNzeHi4uLSq47qlJYWBjOnTsHf39/VYeilJCQEFy9ehUjR45UdSjl2rZtGzp37gxLS0u5+dnZ2QgODkZcXBxMTEzQqlUrhddEe/fuhZmZGTp06KCw/r179yItLU3hsT8rKwu7du0CAPTv3x81atSooq0qP74zZ87g2bNn6NWrFywtLcU4FRk+fDh0dHTE6RevA7W1tWFvbw8XFxdoa2srXL+iy78uO3fuhLOzc4lnFaWRyWTYunUr+vbt+1r+NlR1Hj58iODgYLRp0wYNGjRQdThvnejoaPz9998l/pffRgcOHICLiwucnJzk5kulUly6dAmRkZGQSCRo0aIFrK2tS6x/4sQJSKVS9OrVS2H9J06cQFRUFFxcXNCiRQu5MkEQsGXLFhQWFqJr166ws7NTOu5du3ahTZs2FVqHSGkC0TukdevWgrOzszB+/Hhh3Lhxgq+vr2Bvby80adJECAwMVLi8RCIR/Pz8SpSZmpoKS5YsKTF/7969gkQiEczNzYXU1FSl4tqxY4dw586dCm+PIAhCrVq1hBUrVlRqXXo75OfnC02bNhW2b9+u6lDk3L17V7C2thZiYmJUHco7RyqVChKJRNizZ49KPn/VqlWCjY2NSj672L1794Rt27aVmL9kyRKhXr16r+UzExISBH9/f8HR0VFo3LixUKtWrVKX/eOPPwRzc3NhyJAhQr9+/QQrKyvhyJEjcsvs2LFDqF+/vtCkSRPBz89PaNKkieDm5iZcuXKl1Hrv3LkjmJqaChKJRDhw4ECVbZuyCgoKhK+//lowMzMTevbsKXzwwQdC27ZtBTs7O2HTpk1vPJ5iGRkZwoYNG4S4uDi5+VFRUYJEIhHOnj2r0jiU8fDhQ0EikQjXrl17DZFVnWfPngmWlpbC48eP5eYHBwcL7u7uQsOGDYXRo0cLQ4YMEWrWrCkMGTJESEpKklv2008/LfP/9NNPPxUkEolgb28v5Ofny5U9ffpUMDAwECQSibBq1aoKxb5+/XqhTZs2QmFhYYXWI6qOxo8fL1haWgrjx48XPvnkE6FPnz6CsbGxMH36dFWH9kq2bdsmSCSSN/qZjx49Ev78889Krbt27VqhZs2aVRxR1duxY4fg5uYmSKVSufkbNmwQbG1tBU9PT+HDDz8UevbsKZiZmQnffvttiWOxu7u7MGbMmFI/w93dXZBIJMLQoUNLlG3evFmQSCSCRCIR7t69WzUbpeDzX45v/fr1gqGhoTB37ly55Ro3biyMHz++xE9GRoa43I4dOwQnJyfBxcVFGDVqlDBq1CihQYMGQp06dYTly5eX+PyKLv861alTR/jll18qtI6/v78wZcqU1xQRVZWhQ4cKEolEeO+991QdymsTGxsrbNiwQcjKyqrwusePHxckEomQkJDwGiKrOsHBwYKdnZ2QkpIiN//w4cNC3bp1BXd3d+GDDz4Q+vfvL5iZmQnjx48XcnJy5Jbt37+/0LVr11I/o3///oJEIhHatGlTouzs2bPiMfnle+DyzJ49W/D19a3QOkTKYkMTvVNat24tjBgxQm5efn6+MH/+fEEikQhbt24tsXydOnUEAwMD4datW3JlpTU0+fj4CL179xYsLCyEdevWKRVXvXr1FNalDDY0vfvWr18vuLq6vpUP1oYOHSpMmjRJ1WG8c9jQJAgrVqxQ2NDzOhuaoqOjhR07dgg5OTnCV199VWpDU2hoqGBsbCz88ccf4rzZs2cLtra2QnJysjjPz89PWLhwofi/KZVKBW9vb8HFxUWQyWQl6s3LyxNatWolTJo0SWUNTZ999plgZWUlXLhwQW7+H3/8IUgkEiEgIOCNxyQIRY0fEolEOH/+vNz85ORkYfz48UJISIhK41DGu9LQ9OGHHwoffPCB3Lx79+4JFhYWwvjx4+UahqKjowUPDw+hffv2Ql5enjhfmYYmZ2dnwc7OTjh06JBc2YIFCwRPT89KNTRJpVLB0dHxrXvxguhtNH78eKFRo0Zy81atWiVIJBLh4sWLKorq1V26dEkYP378G/3MzZs3C0ZGRpVa911oaCooKBCcnZ2FjRs3ys3ftGmTIJFISswPCgoSatasKUydOlVuvjINTW3bthVMTEyExMREubKePXuK54Y31dD066+/CgYGBiUaXMrbDkEQhC1btggGBgbC77//LnfNV1hYKKxatUro3LnzKy3/ulWmoenevXuCsbGxEB4e/pqiolf1/PlzwcTERBg8eLBgampa4kWh/4riRpCoqKgKr/uuNDR169ZNrgFcEATh1KlTgqGhoTB//ny548j9+/cFR0fHEo34yjQ0tWrVSjAyMirxYvvHH38sHpMr2tCUlJQkmJqalrjfJKoKTJ1H7zwtLS3MmjULN2/exKxZs+Dr6yuXFqlr16548OAB5s6di927d5dZV3x8PP7++29s2LABlpaW2LJlCz744IMy1wkMDEROTg6uXbuGjRs3AoDYrR8A0tPTERQUhLS0NDg6OqJly5ZQU1Mrs84jR44gIyMDgwcPhoaGBoCiNBTXrl2Dmpoa2rdvL5cyoTiV1eDBg3Ht2jU8ffoUtWrVQps2bUrUHRsbi6tXr6KgoABNmzaFg4NDmbEU1z1w4EBcv34dz549Q506ddCyZUuFyysT56BBgxAUFISoqCh4enoqTPtTXqzKxhUeHo7Tp08DALS1tVGrVi20bNlSrht2RfafIqtWrcKIESOgrv7vsHcvbmtxfLVq1ULr1q2hpqaGe/fu4e7duzA3N0eHDh3kvrPlxSyTybBt2zY4ODjA09NTXO/Zs2c4c+aMXAqr9957Dx988AHmzZsHY2NjhfFHRkbi5MmTGDx4MAwMDOTKdu7cCScnJzRr1qzcv4kib8t+KI9MJsM///yD6Oho1K5dG61atSp1W8r7nijznausqjoOFBYW4ty5c4iNjUXt2rXRtm1bbNq0SUwV+ODBA1y+fBn5+fnice3lFFvFKbwq+v9SFhsbG6VS8m3duhVGRkZ4//33xXkTJ07E0qVLsX//fjFdz9SpU+VSjmhqasLX1xfjxo1DZGRkibR48+fPh0Qiwbhx47B+/fpX3p6Kun//PtatW4eZM2eW2J+jR4/GgQMHMGvWLAwaNAg6OjqIj4/HX3/9hSFDhkBfX19c9sSJE9DV1UW7du3k6khNTcWFCxeQlpYGFxcXuLm5yZXn5eXh/PnzSExMhJOTEzw8PKCmpobs7Gzx/Hn06FEx1ZO/vz90dHTQokULGBkZydUVHR2N4OBg8VjxYorbF1PYpaWl4c6dO9DT04Onp2eJY9CLyopDmc9VJD8/H4GBgahZsya6du0qzr927RoePXoEIyMjdOzYERKJRCw7efIkNDU10bx5c1y9ehXPnz9H48aN0bBhwxL1P3jwAPfv34eBgQGaN2+uMJXRi54/f47du3djz549cvO//vprGBgYYMmSJXLHSRsbGyxZsgT9+vXD1q1bMXr06DLrf5G2tjYGDRqEgIAA9OnTR5y/fft2fPzxx7h582aJdaRSKc6fP4+YmBhYWlqiXbt2csc2TU1NDBs2DKtXr8bQoUOVjoWIinTp0gVA0Tm9devW4vzIyEhcvnwZhYWF8PT0lEsJHhsbi2PHjgEo+h+sVasWWrRoIXdeAIqOL1euXEFeXh7c3NxQt27dEp9/584d3Lt3D7q6umjbti1q1qwplhWnmBsxYgRu3LiBJ0+ewNraGp6ennL3Nubm5nIpfpRdDyhKBRQUFITIyEjY2tqiffv22LZtG1q0aFHq8TwsLAznz5+HIAjiNYuVlZVcCvSytkuRuLg4HD16FM2bNxevI/Ly8nDhwgXExsaiVq1a8PT0lLv237p1K5o3bw4TExNcv34d+fn5aN26dYk0d4IgIDg4GJGRkbC2tkaLFi2gq6tbZjxHjhxBQkICBg0aJM7LycnBrFmz0Lt3b/j5+ckt7+npiQkTJuDHH3/E2LFjyz0XvqhZs2bIzc3Frl27MG7cOABF37/z58/j+++/V3huSE1NRVBQEDIyMlC3bt0SKZ4qY+HChVi0aBF++eWXcu/HX5adnY2ZM2dixIgR+Pjjj+XK1NXVMW7cOLlrpIouX5by9oWy35MXHTlyBAYGBiVSCoaGhuLixYsYOXIkNDU14ezsDDc3N6xfvx5z585VKl56s3bs2AF9fX2sWrUKLi4u2Llzp/h/BsinjXv8+DEePnyIGjVqoF27diXSN6anp+PixYtIT09Ho0aN5O55tmzZgpYtW4r/+8UpTTt16iTev4eGhorH5mLx8fEIDg5Gbm4uPDw8UK9ePYWx3blzB48ePUL9+vXF5wQv1lF8Ttq1axdMTExgYGCAwYMHi8s8efIE165dg7q6Olq1aoVatWqVud/S09Oxd+9eNGjQQDw3FhYWIjg4GBEREbCwsECHDh3k9tH+/fthZ2cHR0dHXL9+HWlpaWjWrJnC9LQ3btzAo0ePxPOXoaFhmfHcuXMHFy5cwO+//y43/8svv4SzszNmzpwpd35r1KgRvv32W0yYMAGnTp1C586dy6z/RVZWVrC2tkZAQAAWLlwIoOiYtX//fnz99dcKj8nZ2dkICgpCYmIiatWqhbZt24rPFQHA1NQUvXr1wurVq6vkHp7oRWxoov8MX19f/PXXX7h+/brcQ2I1NTXMmTMH/fv3x4ULF9C2bdtS6wgICICBgQF69+4NIyMjDBgwAA8fPlT48KjY7du3kZeXh+joaFy5cgUAxAfAp0+fxqhRo2Bvb486dergn3/+Qf369REYGKjwob9MJsO0adMQGBiIHTt2QENDA4WFhZg8eTICAgLEsXYmTJiA3377TTxZHzt2DD///DNWr14NbW1tWFhY4OTJk+jYsSMCAgLE+v/88098+eWX8PT0hJGREebNm4cBAwZg9uzZpW5fcd3Lly+Hrq4uzMzMcObMGXTr1g2bNm0ST6AVifO3336Drq4unJyc0KhRI4UNTeXFqmxcycnJ4t8lLy8PN27cQH5+Pnbv3i1eeCm7/xR59OgRHjx4ID4YeHG//fTTT/jxxx9hZWUFIyMj/PXXXxgwYAD09fVx6dIluLi44OLFi7C2tsbff/8tPjwsL2Z1dXUkJyfjs88+w+nTp+Hq6ors7GwMGTIENjY2cjebXl5eyM3NxYkTJ+Dj46NwGwwMDDB16lSoq6vLPbi/f/8+xowZg4MHDyr1N1HkbdkPZcnOzsaAAQMQGhqKdu3a4cmTJwrHb1H2e6LMd66iqvI4kJmZCW9vb4SFhaFdu3YICwuDmZkZTp8+jc2bN8Pe3h7Pnz9HZGQkpFKp3LYUNzRlZ2ejW7duZX5OcSNIeQYOHFhqI2hpbty4gcaNG8tdMJubm8Pe3h7Xr18XGx4U5bV/+vQpNDU1SzxkCg4Oxu+//45z586V+zLA63Lw4EEIglDquBZ+fn547733cPHiRXTq1AlPnjzBxIkT0a1bN7kHiitWrBAbAYrt3LkTkydPhqOjI+rUqYPp06ejb9++WLlyJYCim83evXvD0tISDRs2xLNnz6ChoYHdu3ejoKAAt27dAlDUcJKUlASgqIEnJSUFEydOxJEjR8SG3WXLlmHu3Llo27Yt9PT0MGHCBIwcORLLli0DUPQAb+LEidi7dy+ioqLg5uaGmzdvIicnB6dPn1aYwxwo+g6WFocyn/uylJQUjBgxAhkZGeK4EwkJCRgxYgQePnyI9u3bIyoqCpMnT8a+ffvg4uICoOjlgvDwcBQUFKB+/foQBAFjx47FvHnzMGnSJLH+iRMnYt++fejYsSMKCwvx5ZdfYu7cuRg4cKDCeICiBjQNDQ25xvPMzEycOnUK48aNUzhOl5eXFxwcHHDgwIEKNTQBwMiRI9GzZ08kJyfD1NQUFy5cQHR0NAYPHozp06fLLfvs2TMMGDAAUqkUzZo1w82bN1FQUIDdu3fLjeXRtWtXLFu2DLGxsaX+LYlIsQcPHgCA2JAkCAJmzJiBNWvWoH379tDW1sbEiROxePFi8diXlpYmnqvz8/Nx584dpKSkYOfOneI4b7t378b48ePRsmVLmJubY8GCBejUqROWLl0KoOg64/3338epU6fQsWNHJCUl4cMPP8SyZcvEsYv++ecfTJ8+HVu3boVUKoWNjQ3OnDkDNzc37N+/X2x0uXz5MiZOnCjGp+x6eXl58PHxwa1bt9ChQwdER0dDW1sb165dw9KlS0u9hkpMTERYWBgEQRD3Q7169dCzZ0+ltutld+/exeDBg9GhQwfxAWzxGE7a2tpo2rQprl27BhsbG+zZs0d8IPnFF1+gadOmiI6ORtOmTREVFYVx48Zh165d4vk4IyMDffr0QXJyMlq2bInnz5/j+fPn2LRpU5n3mwcOHEDLli3lHn4GBQUhOTm51Otdf39/LF68GIcOHcIXX3xRat2KjBgxAgEBAeID8ICAAHh4eCj8Gxw7dgyjR4+Gk5MTateujX/++QcuLi7YtWtXmS+PlGXmzJlYuXIl1q1bV6kxQc+fP4+kpKQy133xGrGiy5dGmX2hzPfkZbdv38batWvx8OFDuZdNZs+eXeI70LVrV+zZs4cNTW+pzZs3Y8CAATA3N0e/fv2wZcsWuYam+/fvY+LEidi/fz+eP3+O+vXr4+LFizA2NsapU6fEF58uXLiAwYMHw8XFBbVq1cIvv/wCW1tb7Ny5EwDwxx9/4Pr16/jpp5/E6V9++QVTp07FnDlzAAArV67E/fv3xeNc8XV0s2bNYGZmhsmTJ+PTTz/FrFmz5GLbs2cPEhIS4ObmBn19/RINTZmZmXj48CEA4NatW9DX14eZmZl4z/rll1/ijz/+gJeXF/Lz8zF27Fh89dVXmDZtmsJ9FhkZCR8fH1hbW2Pz5s0Aihqqhg0bhpSUFLRp0wb379+HIAg4dOgQbGxsAPz7AmFSUhLc3NyQlpaGDz/8EGvWrBFjKSgowNChQ3Hjxg106NAB6enpePr0KVasWCF3Lf6yffv2wdHREY6OjuK8kJAQPHz4EIsXL1Z4LzlixAh8/vnn2L9/f4UamorXnTFjBr777jtoaGjgwIEDkEgkCsdZvX79Onx9fWFiYgJnZ2cEBwfDzMwM+/btk3sJt1u3bpg2bRqkUmm54xcTVYjqOlMRVZyi1HnFrl69KkgkEmHHjh1yy3/00UeCIBR19+/WrZtYpih1noeHhzBhwgRBEIpSFDg5OQmzZs0qNy5FqfMyMjKEunXrCp988onYbTYmJkaoU6eOXBqD4tR5mZmZgq+vr+Du7i6EhYWJ5UuWLBEMDQ3lxr/4448/BBsbGzFF1JIlS0qkDizeHy+m3nB2dhaWLl0qTstkMuHYsWNlbltx3S92C75z545gYmIi93kViXPlypVlfqYysSob18tkMpkwatQoYfDgwSXqKm//KbJ582bBwMBAyM3NlZtfXOeLY9ysX79ekEgkwqhRo8RUXtHR0YKxsbHCMcbKilkQBGHEiBFCkyZNhLS0NGHs2LFCgwYNSqS5EARBaNmyZbn5uv38/Ep0254xY4bg7Owsfn9f5fvzNuyH0ixcuFCwt7cXYmNjxc8ZPXp0idR5lf2elBZ3WV5OnVeVx4G5c+cKderUEce4kclkwocfflhie8tKnafM5zx+/Fhh7vyXf0oba6es1HmtWrUS3n///RLzO3TooHBcgWLPnj0TrK2thVGjRsnNz8zMFNzc3IQffvhBEIR/U6y96dR5Y8eOFczNzUstv3XrliCRSIS1a9cKgiAI58+fV5iWon///uK5TxCKxmszNjYWZsyYIc57+vSpYGlpKezbt08QBEGYNm1aibQwwcHBYkqP0lLWvTxG061btwQjIyO5sTLOnTsnGBgYCLt37xYE4d/9O3LkSKGgoEAQhKK0hS4uLuWec0uLoyKfe+3aNSEsLExo2rSpMHjwYCEzM1Ncx8fHR3BychKio6PFeR999JHQpUsXuWXMzMyEe/fuifOWL18umJmZienroqOjBYlEIpeSIiMjQzh37lyZ2zd+/HihY8eOcvPu3LkjSCQS4ffffy91vT59+giNGzcWp5VJnefu7i4IQlEKojVr1giCIAgTJkwQRo4cKeTk5JRIneft7S20b99e3F85OTlCt27dBC8vL7n0IGlpaYJEIinzeE5ERf/vtWrVEjZs2CD88ccfwpw5c4RatWoJQ4cOFa+P1q5dK0gkErkUl7t37xbMzMzkjlMvmzBhgtx1XZs2bYRvv/1WbpmjR4+Kv69cuVIwMzMT7t+/L85buHChYG5uLqbhKo7lt99+E5cJCQkRDAwM5Op6eYwmZddbtmyZYG1tLZf264svvhAkEomwYcOGUrdVEEpPnafsdhWnzjt+/LhgbW0tLFiwQFw+MzNTqFOnjtCvXz/xej89PV1o3ry5MHv2bHG5mjVrCs2aNRPS09PFeUOGDBF69eolTm/fvl2wsbGRG7fk2bNncvEp0rhxY+Hrr78usW0SiUTuXPSiwsJCoUaNGnJpDJVJnffpp58KUVFRgqGhoRiXu7u78Pvvv4sprYpT56WmpgoODg7CZ599JtYRGRkp1KpVq0S8ynB3dxfMzc1L3NMrWs7Ly0vYsGGD3E/xdWzxvnn69KncegcPHpRbvvjvWdHlFVF2XyjzPREE+dR5cXFxQo0aNcTrGUEoeq5gbGws7Ny5U269Q4cOCRKJpFJjWdLrdf36dUEikYjXgidOnBAkEolcSrTi/7EXx+pLTEwUrKyshNWrV4vzhg4dWmIc8hfTp82bN0+8zhOEonNA9+7dhQ4dOojz3NzchIULF8p97ovH6cuXLwtGRkbC9evX5ZYZP368whTkLyotdd7hw4cFiUQinDp1Spy3detWwcDAQBxD98XUedevXxecnJyECRMmiOPTFRYWCi1bthRatWoljo+Un58v9OnTR/D39xfrbd68ueDg4CDe4wuCIEydOlVwcXERpy9cuCBIJBK58+nz58/LHM9XEIquu1++nyzetr/++qvU9Ro3biz06dNHnFYmdV7//v2FnJwcwcbGRnz20q9fP2HmzJnivU3x314qlQrNmjUTfH19xf2VnJwsuLu7l4i3+P6ivG0lqij18puiiN4NxW+2y2QyheVz5szBhQsXcPz4cYXlly9fRkhICIYNGybWN2TIEGzbtg0FBQUVjuf8+fOIjY3FtGnTxDcarK2t8f777yMwMFBu2djYWPTo0QNpaWk4efKkXA+fzZs3o1evXnJd5f38/KCtrS2XWkcikWD48OHidLNmzaCnp4fHjx+L84yMjHDjxg3xLXA1NTV079693G1RU1PD559/Lk67urqiR48ectuhbJxaWloYO3ZsuZ+pTKzKxAUUvSF55swZbN++HX/++SckEon4xmMxZfafIjExMTAxMVGYFk0ikcilDipO6+fv7y++vWljYwM7Ozs8evSowjGvWrUKANCpUyfs2rULf/75p8K0TBYWFoiOji5zO95//31cvHhRjEMqlWL79u0YOXKk+P2t7PfnbdkPpdm1axdGjhwpvuGjpqZW6pufyn5PlIm7IqryOLB3716MGDFCTNGhpqaGzz77rELxKPM5Tk5O+O2338r9KStVSGlkMpnCN8XU1dVLPQekpaVh6NChMDExEd/gLjZz5kxIJJIKv/Fb1QoLC0ukxXhRcZkgCBWqd9u2bdDU1MQ333wjznNwcICPj4+YZsjIyAixsbG4e/euuEyrVq1gampaoc/as2cPLC0tMWrUKHFe+/bt0aZNmxLH5jFjxojnbm1tbTRv3rzc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RERERPR2Y0JfIiKiaiQxMREbN25EYmKiqkOh16AwMwVCQZ6qwyAiIiIiIiKiaoQ9muidcvHbetDUrL5fW5lMhtiYCFjb1K72A79yXxThfqCKioqKwsSJE+Hu7g5zc3NVh0NERERERERERO84PpUkIiIiIiIiIiIiIiKiSmFDExEREREREREREREREVUKG5qIiIiIiIiIiIiIiIioUtjQREREVI1IJBK0a9cOEolE1aEQEREREREREdF/gKaqAyAiIqI3p169ejh69KiqwyAiIiKqNgplAkLjchWWmUo0YG6o9YYjIiIiIqpabGgiIiKqRmQyGaRSKbS0tKCuzo7NRERERK9bZq4Mh2+lKywb0aYGG5qIiIjonccnTERERNXI7du3YWZmhtu3b6s6FCIiIiIiIiIi+g9gQxMRERERERERERERERFVChuaiIiIiIiIiIiIiIiIqFLY0ERERERERERERERERESVoqnqAIiIiIiIiIiISHmFMgGhcbkKy0wlGjA31HrDEREREVF1xoYmIiKiasTZ2RkhISGwsLBQdShEREREVEmZuTIcvpWusGxEmxpsaCIiIqI3ig1NRERE1Yi2tjZsbW1VHQYREREREREREf1HcIwmIiKiauTp06d477338PTpU1WHQkRERERERERE/wFsaCIiIqpG0tLSsG/fPqSlpak6FCIiIiIiIiIi+g9gQxMRERERERERERERERFVChuaiIiIiIiIiIiIiIiIqFI0VR0AERERERER0dskNCQEd+/egb5Egg4dOkJfX18sy8rKRFDQP0hNSYWjoyNatGylwkiJiIiIiFSPPZqIiIiqEWtra8yZMwfW1taqDoWIiOittHbN71i0aAGSk5MRFxuL2d9+DalUCgBIT0/H1Klf4HxQEPLy87Bu3Rr8tuJXFUdMRERERKRa7NFERERUjVhaWmLq1KmqDoOIiOitdOHCeZw7dxbLfl0BU1MzAMDz5/FQU1MDAOzftxcSfX3MnjMPGhoa6Ny5Kz6bPBE9evZC3br1VBk6EREREZHKsEcTERFRNZKamorDhw8jNTVV1aEQERG9dU6fOon27TsiMiISO3dsx6lTJ2FkZAxNzaJ3NG/duonWrdtCQ0MDAGBnZ4fate1x6+ZNFUZNRERERKRa7NFERERUjTx79gxDhw5FUFAQ3N3dVR0OERHRWyUmJhrP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352EoAcPcOfubZiZmaFlq9aoXfvZBRo2Njb4btESBJ45jeTkZEx+7300aGC4NxMRERmXQqHA3DnfwLZCBUyd+qleD1MPD0/ExcYiPT0d1tY5xZng4CC4u3sUKv6iouzBml8vQYhy/hlqk70LiciYWGgiIiIqRxITE7F+/XqMGzeOhSYiIiIDfKpXh0/16nnGrays0LZd+xLMiIiIXlZqaipmf/M1XKpWxfvvf5irF1LdevVQqVJlbFj/G0aMHIXIyEgcOXwYw0eMLFS8JAlZKgCAyNzyldvQClrcSYkxGHO0kKOSpc0rt01EBLDQRERERERERERERGXI6dMnERISjLCwUJw5fUq3vGPHThg3fiIkEgmmfzEDP6/4CaNGDodcbo1u3bqj3ZMLCQqKlyRBnTO30usUmlLVKux6dN1gbKRnUxaaiOi1sdBEREREVJYYGDOeiIiIiKg86dy5Czp27Jxr+fNDy7m4VMXsOd/m2UZBcSIieoaFJiIiIqIyRCTmxzsiIiIiKt9EIlGu4fKIiKj4cBY4IiKicqRy5cqYOnUqKleubOxUqNjlMYEwEREREREREVER4iWvRERE5YiLiwtmzZpl7DSIiIiIiIiIiKiMYI8mIiKiciQtLQ0nTpxAWlqasVMhIiIiIiIiIqIygIUmIiKiciQ4OBhdunRBcHCwsVMhIiIiIiIiIqIygIUmIiIiojJKJLUwdgpEREREREREVMax0ERERERURonMLY2dAhERERERERGVcSw0ERERERERERERERER0SthoYmIiKgcMTMzg4uLC8zMzIydChERERERERERlQH8lYmIiKgcqVu3Lu7fv2/sNIiIiIiIiIiIqIxgjyYiIiIiIiIiIiIiIiJ6JSw0ERERlSM3b95EjRo1cPPmTWOnQkREREREREREZQALTUREROVIdnY2oqKikJ2dbexUiIiIiIiIiIioDGChiYiIiKgsEUuMnQERERERERERlSMsNBERERGVJSIRACBVk4V0CT/qEREREREREVHxMjN2AkRERERU9BKzlDAzs0AFYydiouLT1EhUaAzG7OUSONpISzgjIiIiIiIiotKJhSYiIqJyxNvbG/v374e3t7exUyEyqkSFBn8EJhmMDW5mx0ITEdEryq+Qr8rSlnA2RERUEK2gxZ2UGIMxRws5KlnalHBGRFQasdBEpUqzbx5A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gwYNgVQqRVpaGrZu2YyzZ88gMzML1apVQ99+/fWKoEEPHmDNmlUICwuFvb09+vUfiLZt2xVLruVZSnIyVq36GVeuXIa5uQUCAtpi2PARJve5qCgkxMfjjz824vLlSxAEAZ6eXhgwcBBq1aqtW+fq1StY99uviI6OQpUqThg2fAQaNWpsxKxf3fHj/2HZD7nnJFy9+hc4ODpCq9Vi8x+bcOTIIahUKtSrVx8TJk6Cg4MDABQYN3Xfzp2NS5cu6i2rVas25n47H2q1Gn/v2Y2jR3MK3lWqOKFz57fRsdOzi8/i4x9j5cqfcevmDVhZWaFDh04YOGhwsZ0zi9Lsb77G1atXci3/fPoM3Wewc+fOYuPv6xEXFweXqlUxatQY1K/vp1u3oLgpYqGJiIiIiIiIiIiIiIiIXknZ6hZCREREREREREREREREJYaFJiIiIiIiIiIiIiIiInolLDQRERERERERERERERHRK2GhiYiIiIiIiIiIiIiIiF4JC01ERERERERERERERET0SlhoIiIiIiIiIiIiIiIiolfCQhMRERERERERERERERG9EhaaiIiIiIiIiIiIiIiI6JWw0ERERERERERERERERESvhIUmIiIiIiIiIiIiIiIieiUsNBEREREREREREREREdErYaGJiIiIiIiIiIiIiIiIXsn/Aa9xLVI+2dpQAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
datasetkeggvariant_effect_codingvariant_effect_non_snv
ref_bpmean4006.04096.04096.0
50%4001.04096.04096.0
max4070.04096.04096.0
var_bpmean4008.04096.04096.0
50%4001.04096.04096.0
max4260.04096.04096.0
ref_dna_tokensmean672.0687.0687.0
50%672.0687.0687.0
max681.0687.0687.0
question_tokensmean216.040.042.0
50%165.039.042.0
max980.082.059.0
answer_tokensmean5.011.026.0
50%4.010.020.0
max10.032.0221.0
reasoning_tokensmean460.00.00.0
50%458.00.00.0
max649.00.00.0
\n", + "
" + ], + "text/plain": [ + "dataset kegg variant_effect_coding variant_effect_non_snv\n", + "ref_bp mean 4006.0 4096.0 4096.0\n", + " 50% 4001.0 4096.0 4096.0\n", + " max 4070.0 4096.0 4096.0\n", + "var_bp mean 4008.0 4096.0 4096.0\n", + " 50% 4001.0 4096.0 4096.0\n", + " max 4260.0 4096.0 4096.0\n", + "ref_dna_tokens mean 672.0 687.0 687.0\n", + " 50% 672.0 687.0 687.0\n", + " max 681.0 687.0 687.0\n", + "question_tokens mean 216.0 40.0 42.0\n", + " 50% 165.0 39.0 42.0\n", + " max 980.0 82.0 59.0\n", + "answer_tokens mean 5.0 11.0 26.0\n", + " 50% 4.0 10.0 20.0\n", + " max 10.0 32.0 221.0\n", + "reasoning_tokens mean 460.0 0.0 0.0\n", + " 50% 458.0 0.0 0.0\n", + " max 649.0 0.0 0.0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "share of reference spans truncated at 1024 tokens: {'kegg': 0.0, 'variant_effect_coding': 0.0, 'variant_effect_non_snv': 0.0}\n" + ] + } + ], + "source": [ + "from transformers import AutoTokenizer\n", + "llm_tok = AutoTokenizer.from_pretrained(\"tokenizers/prism-olmo-1b-interleaved\")\n", + "dna_tok = AutoTokenizer.from_pretrained(SNAPSHOTS[\"dna\"], trust_remote_code=True)\n", + "\n", + "LEN = H.length_frame({k: DS[k] for k in (\"kegg\", \"variant_effect_coding\", \"variant_effect_non_snv\")},\n", + " llm_tok, dna_tok, max_rows=1500)\n", + "H.plot_lengths(LEN, max_dna_length=1024); plt.show()\n", + "display(LEN.groupby(\"dataset\").describe().T.loc[(slice(None), [\"mean\", \"50%\", \"max\"]), :].round(0))\n", + "print(\"share of reference spans truncated at 1024 tokens:\",\n", + " LEN.assign(t=LEN.ref_dna_tokens > 1024).groupby(\"dataset\").t.mean().round(3).to_dict())" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "cell-016", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "37 distinct KEGG answers; top 15 cover 85%\n" + ] + }, + { + "data": { + "image/png": "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wsPD8fb2xtXVlQoVKvDAAw9w/Phx42kRAC1btsTT05MJEybQsmVL3N3dcXd3p1atWrz55psEBgZSu3Zt4PrGpCix5KVFixb89ddfFvuudYxcXV2xt7fP9Tr079+fKVOm8MwzzxhPZriyX8XVr18/PvroI3755ReGDh1q7A8NDc3zykVR2o+NjcXDw8Pij7Sff/652LEVh4+PD3/88Qf33HMPPXv2ZNmyZVSoUAEfHx86dOhA5cqVmT59usU5EREReHp64uLiQqtWrVi4cCF33XWXcfzq92NRyxUmPT2dhIQEiysw8fHx/PHHH4U+GeNqly5dIj09nXLlyuHj40P//v1JT09nxIgRpKSk4OrqipeXFy1atOCjjz7C1taWZs2aYWNjQ3BwMO+99x4ZGRnG51tRxqswp0+fJikpiebNmxerL0pkRW6wy8nsfffdxz333MPChQvp1KmTcXzBggW5LvtcfnxTUFAQixcvZtiwYbRu3ZoBAwZQvnx5YmJi+Pvvv0lLS7P4EChu+by0adOGpKQkxo4dS926dalSpUqed+Bfds899zBt2jQyMzPx8vJi0aJFxV7TlJ+iPrXgerz55pt06tSJO+64gyFDhhAWFsbXX3/NuHHjqFatGllZWQwZMoQGDRoYv1DnzJlD69atjbEs7pgVhbOzM9999x3Dhg1jy5YtNG/enNTUVHbu3El0dDRr1qzB1dWViRMn8sQTTxAdHU2dOnVYunQply5dylXf448/zhNPPIGnpydVq1Zl1apVHDlypNhtFke7du348ssv+fTTT/H09Mz38Vv169enfv36DBs2jOHDhxMbG8u3336b67FGRRESEsLTTz9Nz549qVGjBpGRkXz11VcWM1b29vZ07NiRP/74g9dff93Y37VrV7744guLpzhcz5gUJZa89OzZk2nTphEVFWVcFbjWMbK1taV169Z8/vnnxMfH4+joyH333Ufz5s2ZMGECPXv2ZOjQoVSrVo2oqCjj8vCVd7MXRfPmzRk/fjxPPvkkGzZsoF69ehw5coSzZ8/muaSpKO3369ePDz74gPvuu48uXbpw6NAhfv/991z3AdxoHh4e/Prrrzz00EPccccdLFu2jOrVq/PFF18wYMAABg4cSHBwMHZ2dhw4cICtW7eyZs0a3NzcmDhxIn369OHee++lY8eObNu2zfhSkCtnHotariBO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e5ak5CQqVQoiKzOT2NhYYw3r8ePHKFu2nHETUkpKCqdOnaRBg4bG+WFhodja2hIYWMGi3lEjn6RXr94MGDgoV5uZmZmcDwsDGxsqVKiQ63mtMTEXiIrKmdWrUbNmrjguS05O5vz5MDw9vShfvryxPy4ujscefZjvvp9NZkYGF+MuEhRUGScnJ6P9w4cPUb9+A+OXwaVLl4iICKdOnZw1uwcPHqBevfoWsaWnp3M+LAwXV9d8Z3bzOjcjI4OzZ88AUKVKVSLCw3F0crKI+dzZs1z6/+Tz8v7MzEwiwsPJys6mUqVKRn15xV9YO/n16erYw8JCyczMpGLFSrkeop1X//Ort7ixA8TGxBAbG0v5gADjsWdXtn327BmcnZypWKmS8YdPfHw80VFRFs+Sza9sYUwmExHh5xg0K5UUPdZXRG4Dhyb++/jAy7+jAgKDivx77XZUWvohN0exE9mbYd++vXz4wfvMnPU1Hh6ehZ9wg12ZyF6dEInkR4msiNxulMjKf02xnlpwM7z7zlscO3aMO++8q0SSWBERERGxTiWeyA4cNBgvL2+CgoJKLAZ3d3cmvPGW8WQEEREREbn9lXgie+VD+UuKg4NDri9UEBEREZHbmxabiIiIiIhVUiIrIiIiIlZJiayIiIiIWKUSXyMrYu22TKhZrIeN325Kw6NtSkMfoHT0ozT0AUpPP0RKO707RURERMQqKZEVEREREaukRFZERERErJISWRERERGxSkpkRURERMQqKZEVEREREaukRFZERERErJISWRERERGxSkpkRURERMQqKZEVEREREaukRFZERERErJISWRERERGxSkpkRURERMQqKZEVEREREaukRFZERERErJISWRERERGxSvYlHYCItWv79glSMks6ihvhWEkHcAOUhj5A6ehHaegD3Kp+HJpY95a0I1LaaEZWRERERKySElkRERERsUpKZEVERETEKimRFRERERGrpERWRERERKySnlpQCl26dIlL8fEEVa58TecnJiYQG3uRKlWq5LktIiI3X3x8PN98842x3a1bN5o0aWJRJj09ndWrV3P27FmqV69O9+7dsbX9d44qIyODZcuWERYWRo0aNejRowc2Nja3qgsiN51mZPOQnZ3N8WPHyMy0zmcq7dq1k88+m3bN5+/bt5epUybnu22tEhMTOHPmTEmHISJSJNnZ2TkT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MqEm9oV8ve3tTI+2ERERa6VPLRERERGxSkpkRURERMQqKZEVEREREaukRFZERERErJISWRERERGxSkpkRURERMQqKZEVEREREaukRFZERERErJISWRERERGxSkpkRURERMQqKZEVEREREaukRFZERERErJISWRERERGxSkpkRURERMQqKZEVEREREaukRFZERERErJJ9SQcgYu3avn2ClMySjuJGOFairR+aWLdE2xcREeujGVkRERERsUpKZEVERETEKimRFRERERGrpERWRERERKySElkRERERsUpKZG9jJpOJiIgIsrOzSzqUIktNTSUm5kJJhyGl0IULF/j+++/ZtWuXxf6kpCRWrVrFX3+t5PTp0yUUnYiIlAQlsrex1NQURj/9FPFxcSUdSpHt2rWDd995u6TDkFLo2Wef5cUXX2Tp0qXGvvnz59OmTRs+++wzfv/jD9q2bcvbb+vnT0Tkv0LPkZV8paamkpychJ9f2ZIORf7j5s6dS1paGi1atLDYX6ZMGTZu3IiXlxcR4ec4cjSEQYMGMXz4cCpXrlxC0YqIyK2iGVnJ1/bt25j4/nslHYb8x4WHh/POO+8wffr0XMfuuOMOypQpY2xXr14dgISEhFsWn4iIlBwlsrdQcnIyaWlpeR6Lj48nKyuryHXlVT47O5uIiAhMJhMmk4no6GhMJpPFOQWtt710xfHMzEwuxceTlZVJREQEERERZGZmkp6ebmxfunSpyPEW1h5QaN2pqSnExsQYZRMT809Wrq77SnmNw5V1Z2RkEBsbW+z+yM3x9NNP8+KLL1KxYsVCy06fPp26detSr169WxCZiIiUNC0tuAUOHjzAV7NmEBsbi52dHV26BDP84UewtbVl69YtfDVrJiZTNmlpaXTq1JlHH3sCR0fHPOsqqHxCQgKjn36KgYMGs3bNalxdXfl48hTWrFnN/F9+wd7enrS0VPr1H8B99z1g1Pn7st9YsOAXbG3tADOPPf4kgYGBLF36K0lJSbz7zpsAvPjSy6SnpzP906kAJCUl4+7uxnP/e4FatWoXeTzyaq99+w6cPn2qwLq3bNnCkl8XU7lyZQ4fPkxKSjK1atVm/Kuv4+TkVGDdAH/8sSzfcbhcd6VKlTh69Ah169bjxZdetog7MzOTzMx/v4vWbDYhN9c333xDRkYGI0aMKELZ7/j1119ZsWIFdnZ2tyA6EREpaUpkb7LYmBg+mPge99//IH369iM7O5vlf/5Benoaly5dYuqUyYx+5lk6dOjExYuxvDHhNRYtWmCRaF4WGRlRpPKxsbHM+upbHBwc2LJlM4sXLeTd996nUqUgYmNieO318VSuXIV27dqz6Z+N/PLLPCa88SY1a9YiKSmJtWtX0759B4YNH8FvS5cw+ZOpFnF8/sVM4/9//vkH0z+dyrRPP8fWtvAJ/vzaA6hTp26hdZ8/H8bd9wxlzItjSU5OYuxLY1i/fh09evQssO7CxuFy3QMGDOSlsa/kGfviRQuZP3+ese3s7MSkSZMK7bNcG5PJxKuvvspTTz3FDz/8AEBUVBQHDx5k/vz53HPPPUbZSZMm8f0Ps1m+fDm1atUqqZBFROQW09KCm2zb9m2UK+dPn779ALCzs6Nf/wG4uLiyc8cOKlasRIcOnQDw8fGlb9/+bN2yOc+6ilp+yJC7cXBwAGDd2jW0adMWe3sHIiIiyMjMpEWLluzYsR3ASAJr1sz58Hd3d6d//4GF9is1NYWoqCiaNGlCdHQ0Fy8W7VJ8UdorqO7yAQF07JjTfzc3d+rVq8/582GF1l3YOAD4+vrS/Y4e+cZ+55C7mP3TXOPfrK++LVKf5doNGTKECxcusGPHDnbs2EFCQgLh4eHs37/fKPP6668ze/Zs5vz8g5JYEZH/GM3I3mTx8XH4+PrmeexSwiV8fHws9pUpU4b4+LzXnha1vLe3t/H/ixcvcvz4cfbu3WNRplbtOkZ8jZs0KUpXAAgLC+XTaVMICwvDw8MTe3s7TCYTcXFxRXq6QUHtFaVuN1dXi3Ps7e2NtcIF1V3YOIDluOXFwcHB+AMBcmYM463nyWhWx9bWls8//9xiX69evWjVqpXxiK133nmHadOmMXbsWLZu3caRoyHY2NjQtWtXgoKCSiJsERG5hZTI3mSBAYGsXbOGzMxMIwkym80A+PuX55+NGzCZTMal8zNnzlC+fPk86ypueYDAChWoV78+I0Y8arH/8s1OAYEVOBkSkuuYnZ0dtra2mK5aBzrn55+oXqMmEz/4CDs7O1JTUxj20AOYTeYijUdB7d3MugsbB7EOvXr1omrVqsa2n58fDz30EOHh4aSkJOHq6o6NjQ3NmjVTIisi8h+gRPYma9e+AwsXLmDKJx8zYMAgsk3ZLPl1Mc+/8CLt27dn7pyfmTVzBj169ORc6DmWLVvKk0+NyrOu4pYHGDx4CK+OfxlfH18aNmpEcnIy27Zuwb98AP369WfQoMGMe2Us5QMCaNGiFVGREezYsZ1nn3uecuXKcSE6mmPHjuLp6YWfnx+Ojo5ERUVy9swZMjIzWLRwgZGYF0VB7d3MugsbB7EO//vf/yy2R44cCfz/t+CFnyMgMKhIa7VFRKR0UCJ7kzk6OvL+xA9ZMH8e3333DS6uLgwcOBjX/79EPvGDSfwybw5ffDEdT08vRo4abdxlb2trS/ny5bH9/zuwXVxcCyxvZ2eXU/6KD/IqVarwwYcf8eviRWzcuAE3NzfatGnLHT16AlC1ajXenziJXxcvZPu2rQQGVuCBB4cBULt2Hfr07cdXs2aSmprCiy+9zPDhI/jxx+/59NOpuHu407tXH+Li4nBwzJltdnF2xa9s/ksMCmqvsLpdXV3x9fWzqM/Tyws7W7tC6y5sHPKqW0RERG5vNubiTHmJiOHyLOCgWamkZBZeXgp2aGLdaz63tMzIloZ+lIY+QOnoR2noA5SefsjNoZ8IEREREbFKSmRFRERExCopkRURERERq6REVkRERESskhJZEREREbFKSmRFRERExCrpObIi12nLhJrY21vvW0mPthEREWulTy0RERERsUpKZEVERETEKimRFRERERGrpERWRERERKySElkRERERsUpKZEVERETEKimRFRERERGrpERWRERERKySElkRERERsUpKZEVERETEKimRFRERERGrpERWRERERKySElkRERERsUpKZEVERETEKimRFRERERGrpERWRERERKySfUkHIGLt2r59gpTMko7iRjhW7DMOTax7E+IQEREpGs3IioiIiIhVUiIrIiIiIlZJiayIiIiIWCUlsiIiIiJilZTIioiIiIhVUiIrIiIiIlZJiawV++67bzh79swNqy86Ooovv/jslrX39VezCAsLu2H1ye1h/fr13HvvvbRo0YLBgwezcuVK45jJZGLu3LkMHDiQli1b8sADD7B79+4SjFZERKyZElkrtnnTJi7GXixy+cIS0YSEBNavX3fD2ivMP/9sID4+7obVV5jjx48xZ85Pt6y9/6JDhw7x2Wef8fDDDzN79mx69OjB0KFD2bRpEwAzZ85k/fr1PPvss3z//fdUqlSJ3r17c+rUqRKOXERErJG+EAH45puv6NGjJ5UqBZV0KDdV7dp1cHf3KOkwSoyXlzc1atQs6TBKtTp16rBgwQJju27duvzyyy+sX7+e9u3b88QTT2BnZ2cc/+CDD5g/fz4bNmygWrVqJRGyiIhYMc3IAps3/cPFizdupvF21a5de3x9fUs6jBLj7+9Pq1atSzqMUu3KJBXg6NGjnDhxgtatW+d5PCIigpiYGKpWrXrLYhQRkdLDamZk5879mZiYGOxsbSnn709wcLdcSdmhQwfZt28vWVlZdOrYmSr//+E4ffo0HnzgIcr4+AAQGRnB0iW/8uRTo1i0cAEpKSksXforGzasp0aNGvTu3ReAgwcPsG/fXjIzMmncpAlNmzbLN76vv5pFly7BnAg5TlhoKBUqVqJnz16EhoayYcM6srOzCe7S1Yhp3do1ZGRm0qNHT6OO2NhY5s75iUcfexwXF9dixwCwdesWjhw5jJOTE61atqZGzX9nIL/77hu6du1G5cpVADCbzaxdu4bTp07iXz6AqlWLPyNWUHwFjXteVv29kugL0dxzz73Y29uTmprKunVrOB92Hh9fH7p17Y6XtzcAR44c5sD+/TRp2pSdO3eQmppKly7BVKlSlbVr13DyZAiBgYH06tUHBwcHIGdpwc6dO7j//gcBCA09x5IlvwLg6upK7dq16dChU7HHQHLr27cvx44dIzY2ljfffJNu3brlKpOens6IESPo1asXnTt3LoEoRUTE2lnNjGyN6jWoX78BtWrVJjoqijEvPEd8fLxxfO6cn/lo0gfY2tri5enFjBmfExp6DshJGpNTUoyyCZcS2LBhPQBVq1XD3t6eKlWqUr9+A2N5wfxf5vHptCk4Oznj4+PDjC+/YOn/Jz15+eefDXz44URiY2MJCAjk18ULmfj+u3z5xWf4+vpiys7m1VdfIe7/Z359/fz4+acfSU9PN+r4++8VREVFGUlscWOYOeMLvvv2G3zK+GAymXjjjdf4558NxvGr17h+/tmnLFq0gHL+/ly4EM3UKR8X+jpcqbD4Chr3K5lMJr777hv+/PMPevbohb29PQkJCbzwwnMcOHCAChUrEBUZyQsv/M94zSMiIli2bCnff/cN3l7epKel8dqr45j4/rucOnmSoKAg1qxezddfzzLaiY6OYvu2bca2u5s79es3oH79BpQrV44F83/hm2++yre/mZmZpKSkGP9SU1PyLftf99VXX/Hnn3/y5ZdfMnnyZObOnWtxPDU1laFDh2Jvb8/3339fMkGKiIjVs5oZ2ZZXXBLufkcP0tLTWbVqJXfddQ+RkZEsXryQDz78iOrVawDQp28/iyQxP82aNcfR0ZHGjZvQuHETICfhWbhwPlOmfkqFChUBaNCgIa+9No4+ffsZM3xX69evPwMHDQbA1s6OH3/4jllffYunpycAhw8fZu++vQQHd6Vhw0Z4e3uzadM/dO3ajezsbFavWsWDDw27phjOnw9j1aq/+WTKNCMZ9ynjww/ff5fnLGNYWBgbNqzn0+lfUL58eQCcnZ0LTJSvdK1jdLWMjAw+/uhD0tLTeO/9iUYSv3DBfIIqBTF27CtG2SlTJrNs2VIeemi4se+119/E1TXnnP3791O2bFmeePIpAAIDKzB1ymRGjnw6z7bL+PjQteu/M4Xt2nVg1MgnuP/+B3FxcclVfvGihcyfP8/YdnZ2YtKkSUXq539NYGAgALVq1WL79u0sWLCA++67D4DExETuuusuPDw8mDdvHs7OziUZqoiIWDGrSWRjY2PZuHE90VHRpGekE34+zEiWTp8+hZeXl5HEAjg4OBQ5mbraqVOnsLOzY/HiRRb709PTiYqKpGLFSnmeV616deP/Zby98fMraySxAN7e3iQmJBjbPXr2YuWKv+jatRs7d2wnIyOdtm3bXVMMZ8+exdu7jMUNa40aN+abb74iISHBIo6c8mcoW7askcRCTiJa1ET2Wsfoap9Nz0m8J7zxlsX6yZCQ42RlZTN9+jRjX0R4OCnJyca2v395I4mFnPGtWu3K16AMCVeMd1727N7NwYMHSEhMwGQyAXAhOpqgypVzlb1zyF30HzDQ2DabTcTHxRSpn/8VCxYsoGzZsnTq1AlbW1tCQkJYvXo1gwfn/IF38eJFBg8eTFBQEN9+++01v0dFRETAShLZS/HxvDjmfzRr3oIaNWrg5ORMUmIiaampADg6OpKRkZHv+TY2NpjNJmM7MyuzwPYc7B2wt7enfv0GFvvr12+Al5dXge1YbNva5CpjNpuN/3fp0pU5P//E6dOnWLHyLzp16oyjo+M1xeDg4EBmpuUYZGRkGseuljNmluOQmVnwuFi0V4T4ijLuffr04/fff2PTPxvp1LmLsd/ewQE/v7IW9dev34Ay3mWMbds8xtfWJve+/Py6eBF//bWc7t3voHxAAHZ2dvyzcQNpaWl5lr/6jyOTycQtfHqYVWjVqhUvv/wyDzzwAPb29mRkZHDfffcxbtw4AL744gt27dpFZGQkjRs3Ns57+umnefrpvGfORURE8mMViezpM6cBG55++hlsbW3JzMxk5Yrl+Pr6AVCzZi3jxqXg4K4AREZGYmtrQ7ly/vj6+nL27BljtnLb1q0W9Tu7uFgkLzVr1QJs8PHxoUmTpkBOArpt21Y8PCxnNq+Hm5sbHTp24uefZrN/3z6GDx9xzTHUqFGT9PR0dmzfZizDWL1qJdWqVc/zMnmNGjVJSkpk//59NGqUk1CsW7e2yLEXJb7Cxh2gTt26NGvWnHfffYu0tDR69OwFQJMmTdm2dSsdOnQ0kvu4ixeJio4qcoyFOXBgPz179uLOIXcBsH//PrKysm5Y/f9FlStXZt68eaSlpZGUlISfn5/F8dGjR/PQQw/lOq+gPxBFRETyYxWJbM2aNXF2dmLsS2MIDAzk5KmTuFyxrs7T05NRTz/Dl198xprVq3BxdSU2Jobxr74OQP8Bg/jyi8/ZsnkzsbGxODk5WdTfpElTvv/+W7Zt3ULNWrXo3bsvzz73Pz6dNoWKlSrh6eHJ6TOnadSoMW3atL2hfevVqzcvjnmeWrVqG08TuNyn4sRQpkwZHnn0caZNm0K9+g1ISkwgOjqaV8a9mme7ZcqUYdjwh/nwg/dp2KgxF2NjcHIq+lrFosRX2LhfVq16dd56+13efusN0tLSGDBwEAMGDOLM6dOMGvkEtWvXITklmdiYGEaOGl3kGAvTtl17vvv2a0JCTpCRmUl83EUjaZbr4+zsnOfaV29vb7z//8kTIiIi18vGfOW17ttYeno6Bw7sJz0tjZq1apGSkkpSUiINGjQ0yiQmJhIScgKz2Uy9evUtPkjPnDlDRPh5KlSshJeXF/v27jEuZZvNZg4fPkxMzAV8fX2NOlNSUjh5MoTk5GSqVq2Gv79/vvH9888G6tdvSJkyOZe+o6OjOX3qJK2vSDr37d1LGR8fgoL+XceanZ3Nw8Mf4uERj9CtW/dc9RYUw+bNm6hdu47FY8hiY2M5eTIEJ0cnataqZbGGNK/yERERnDlzmoDyAfj6+bJr1y66dAnOs495nV/YGBU07rnHLIpDhw7RsmUr3N3dgZyb2MJCQ3F1c6NWrdpGMhwREcH582G0aNHSaGvXzp0EBAYaNxolJyexbds244auqKgozp49Y/Es2bCwUM6cOY27uwf16zdg65bNNGrcpEgzhCaTiYjwcwyalUpK0VdllCqHJtYt6RCAf1+LgMAgbG2t5mEsuZSGfpSGPkDp6Edp6AOUnn7IzWE1iWxptWXLZmZ8+TkzZ32ju7etjBJZJbI3WmnoR2noA5SOfpSGPkDp6YfcHFaxtKA0Sk1N5atZM9izZzd33nmXklgRERGRYlIiW0Ls7e1p2KgxXbt1t1geISIiIiJFo0S2hDg4OBhPWBARERGR4tNiExERERGxSkpkRURERMQqKZEVEREREaukNbIi12nLhJrY21vvW0mPthEREWulTy0RERERsUpKZEVERETEKimRFRERERGrpERWRERERKySElkRERERsUpKZEVERETEKimRFRERERGrpERWRERERKySElkRERERsUpKZEVERETEKimRFRERERGrpERWRERERKySElkRERERsUpKZEVERETEKimRFRERERGrpERWRERERKySfUkHIGLt2r59gpTMko7iRjhW7DMOTax7E+IQEREpGs3IioiIiIhVUiIrIiIiIlZJiayIiIiIWCUlsiIiIiJilZTIioiIiIhV0lMLrpKenk5aaipe3t4lHUqpkpiYiJ2dHa6uriUditwChw8fJiQkhEqVKtG0aVOLYyaTiT179hAeHk6tWrWoXbt2CUUpIiLWTonsVTZt+oe/lv/BpI8+uWVtpqenk5qaincpTp6/mjWD8gEB3H//g/+J/v5XxcbGMnz4cC5evEjlypXZtWsXVapUYfHixbi7u7Njxw5Gjx6Ns7MzAQEBbNy4kZ49e/L1119ja6sLRCIiUjz65LjF0tPTuRQfb7Fv/769TJ06+aa2cTu50f2V20d6ejpvvPEGmzdvZu7cuezevZvTp08ze/ZsADIzM5k9ezbr169n3rx5bNq0iSVLlrB8+fISjlxERKyRZmRvsa1bt/Db0iVM/mSqsa9lq9a0bNX6prZxO7nR/ZXbR2BgIIGBgca2u7s7vr6+pKWlAdCuXTuL8pUrV8bd3Z24uLhbGqeIiJQOSmQLkJWVRWZmJi4uLsa+1NRUsrIy8fDwNPZduf7zyjW2V5+fmZlJclISWdlZxMbEAODp5YXJZLK41F5QHVfHl52djZOTkxGDg4NDnm04ODgY5yUnJ+Hm5m5R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YhR79uwmOjqaVq3aKMSKiIiI3CYFWTsxGo3UqlXb3mWIiIiIOCwtNhERERERh6QgKyIiIiIOSUFWRERERByS1siK3KXNQ8rg4qIfJRERkftNM7IiIiIi4pAUZEVERETEISnIioiIiIhDUpAVEREREYekICsiIiIiDklBVkREREQckoKsiIiIiDgkBVkRERERcUgKsiIiIiLikBRkRURERMQhKciKiIiIiENSkBURERERh6QgKyIiIiIOSUFWRERERBySgqyIiIiIOCQFWRERERFxSC72LkDE0dUbcZREk72rsHVwdHl7lyAiInLPaUZWRERERBySgqyIiIiIOCQFWRERERFxSAqyIiIiIuKQFGRFRERExCHpqQX3WWpqKq6urg57vjiurVu3EhcXB0CePHmoVavWDW1iYmI4dOgQ3t7eVKpUCYPBcL/LFBERyTbNyN5H0dHRPPlEd2uYuF0JCfE8+UR3Ii9ftu5LTU29q/PtxWw2YzI9YM+syuEWLFjAlClTePvttxkwYMANx2fMmEH58uUZNGgQPXr0oGnTpkRGRtqhUhERkexRkL2Hbidk3omEhAR693rKIcPGli2bGfDhB/Yu46Eybtw4fv75Z7p163bDsfDwk7z77rvMmjWL1atXs2/fPry8vBg6dKgdKhUREckeBdl7JC4ujief6E50dPQ9u4aXlxdz54WQP3/+e3aNe6V+/QZ8Ou5ze5ch/9i//wA+Pj60bNkSAKPRSJcuXVi0aBEWi8XO1YmIiGROQfYfJpMJs9lss+/aGdVrj5vNZpu2mf1Dn5Zmsp6XmppKenr6DW1uFhCyGx6un/W92fXu1q36vraW69tcP57rlxakp6fbbN/uvbnV+ZmdYzabSU1NJTU19YbX/mGTJ08e4uPjiYmJse47c+YMcXFxXLx40X6FiYiI3IQ+7PWPV195idf7vkGVKlUBiLx8mRdffJ5ZP/yEl5c3/T94jypVq/L3wYOcPn2K115/g7x58/LdtzM4deokPj65aNmyFY91646zszPvvvMWAG/0fRWAbt160LxFxmzXhvXr+P33X7l8+TKBgYG82e8dfH19ATh9+hTfzviGI0dCcXExUr9BA5599jnc3NxuqDkhIZ7evZ5m+vQZ5C9QAIBnn+lJeno6FouFQoUK0bPXM9SpUzfb9+HUyZPMnPktR46E4ubmRqdOXejYqfMt+46OjuZ/zz9Lnz7Ps3Tp70RFRfHNjJns27uH+Qvmcf7cOXwLF+aFF16iUqVgNm3aQMiCBYyfMAmAv/76k99+/YXKlauwbt2fJCcnU7duPV57/Q2cnZ0BWLr0d35Z8jNRUVEULlKEnj17U6tWbev5v/66hKAyQWzbto2kpERatmxFs+YtmPrlFE6cCKNw4cK82e9tSpcuA8DBgwcY9dEIAAwGA2XKBPHSy69QtGixTO+NyWS6LiznnPBbp05tgoKCeOKJJ3jxxRc5efIkc+bMAdBaZhEReWApyN6G9evW8eGHAykVEEBMdDSvv/4Kjz/+JG3ajubM6dOMHj0Kdw8POnbsxPgJk+nzbC+mTvuavHnzAliXGezfv4+PR4/F1dWVL774jDmzf6TfW+8QHx/P0CGDaN+hI/0/HEhSUhLjv/iMObN/4tk+z2Wrxp9mzwMyZih3797FhPGfExAQSMGCBW957pUrcQwdOphmzZvzzrvvk5aWxs+LF5GQkICXl1e2+t69exejPv6EPHnysGfPbiZOHM+b/d6mRo2aXLx4gT//XEulSsGZXv/kyXAaN27CtK++ISoqisGDPmT9+nU0adKUTZs2MvunH/mg/wDKli3H5k0bGffpJ4z99DNKlvQHMkJ482Yt+N8LL3HyZDgDB/Rn3769vPX2exQrVowffvieb76ezphPPgUgOLgyc+eFAJCSkkLIgvmM/+LzLJc8LFoYwvz5c63b7u5ujB079pb31RG4uhpZtmwZU6dOJSQkBD8/P4YMGULfvn0pVKiQvcsTERHJlJYW3Ia2bdsREBiIwWBgy9bNFChYkI6dOuPq6kpAYCAdOnZi7drVt+zn6Z69yZ07Nx4eHjRq1IiTJ08CsGnTRnLnyUP79h0xGAx4enryWLfurF+/7o7qrVy5CkFBZdm/b2+22m/etAlvby969uyNl5cXuXPn5pln++Dl5ZXtvp/u2Zs8efIAsHzZUlq0aEnduvUwGo0UK1acnj17Z3n9ggUL0alzF1xdXSlcuDCVq1Tl5MlwAP78cy3Nm7ekUqVgjEYjjRo3oWLFSvz115/W8wsVKkT7Dh0xGo2ULl2G4sWL06RJM/z9/XFxcaFRo8bW/q7n7OxM18e6cerUSaKjojJt0/Wxbvzw4xzr1/Svv836ZjogFxcXBg4cyOzZsxk3bhzLly+ndevWmb4bICIi8iDQjOxtuPr2PUBUZBS+hXxtjvv6+hKVjScI5MqVy/q90ehKamoKAJcuRXDm9Gl693rKpr3BYCAtLe2W/aanp/PjD7NYv/4vYmNjcXZ2Jj09nfLlK9zyXIBLly9RuHCRTJ8dmt2+C1xzjy5fvkxwcOVsXRsgVy4fm21Xo9G67jYqMpKqVavZHPf19bV5YoOPj+35Li5Gm31GF6PNOt7IyEi+/vorDh7YT3JyMs7OzqSlpREZFUnefPluqM9oNGI0Gq3bZrOZmHv3Wb7/3N9//83Zs2cJDw8nJiaGlStX4unpSb169QB49dVXqVWrFn5+fvzxxx9s2rSJ1atv/YuZiIiIvSjI/sPNzc0m5ETfIqHkL1CALVs32+w7d/as9W12axi8jU98FyrkS6lSpRj7aeZvbaek3Pz8dev+Ytu2LYwYOYoiRfwwGAyMGD402x/88i3ky8YNGzCbzTg52U7W30nfhQoV4vSZ09m69q0UKFCA8+fP2ew7e+4sQUFl77jP77//DldXVyZPmUbu3LkxmUz0fPoJzOk5Z+3rtdauXcvKlSsBKFq0KFOmTKFIkSLWIDtp0iQmTJjA5s2bqVixIp988omWFYiIyANNSwv+ERAQwMqVK4iOjubs2TPMmjXzpu3r1atPbEwM8+bOITYmhv379/HLL0to0aIVAN7e3ri6unLk6BFSUlKyFSbr129ATEwMP/34A5cuXSI6OpqNGzcwdeqUbI3BZDJhsWR8Qj8+/gorli9j//592ToXoF79BqSmpvDN19OJiIjgwoXzTP1yMgkJCXfUd7tH27N2zWpWr15FbGwsR0JD+ebr6dmu51rNW7RkzepVbN+2ldjYWJYu/Z3Qw4dp2rTZHfUHkGYyYTGbwWLh0qVLfDXty2zNfDuq1157jZ9//tnma+rUqdbjuXPnZtiwYcydO5fBgwcrxIqIyANPM7L/6NX7WaZ+OZl+b/bF19eXNm3aciQ0FMiYWTUaXWxmKTP+0R/JrFkzWbbsD3LlykWXro/RqnUbIGNG9tk+z/HdtzOIjY3hsce606JlK4xGI9e+c+/k5ITLP29Xe3l58dGo0cz+6UcGDxpAenoa5StU5Mknn/6ntSHjrW1rB7bbTZs2IzT0MIMGZvzVpho1alCzZi3rp/5vPN+Wl5cXIz8azQ+zZjLgw/fx8PSkc+eueHl53bJvg8Fww9gqVqzE+x98SMiC+fz4w/f4+RXluef/98+4nW3epndycsLF5d9tAGdnF5ydM+55rVq1eeGFl5g7dw6RkZfx8/Nj0OCh1icMZHa+0WjEyTp2MDgZbK7Zs1dvpk39ktdffxVvH2/atmlHnjx5MDjpz7KKiIg4AoNFTzsXuSNms5nz507ReXoSiQ/YE6oOji6f7bZXx1HEr8QNS0ocRU4YA+SMceSEMUDOGEdOGAPknHHIvaH/IkRERETEISnIioiIiIhDUpAVEREREYekICsiIiIiDklBVkREREQckoKsiIiIiDgkPUdW5C5tHlIGFxf9KImIiNxvmpEVEREREYekICsiIiIiDklBVkREREQckoKsiIiIiDgkBVkRERERcUgKsiIiIiLikBRkRURERMQhKciKiIiIiENSkBURERERh6QgKyIiIiIOSUFWRERERBySgqyIiIiIOCQFWRERERFxSAqyIiIiIuKQFGRFRERExCEpyIqIiIiIQ3KxdwEijq7eiKMkmuxbw8HR5e1bgIiIiB1oRlZEREREHJKCrIiIiIg4JAVZEREREXFICrIiIiIi4pAUZEVERETEISnIioiIiIhD0uO3cojU1FRCQubTuXNXPD09M20TFhbGkSOhtGnT9j5XJ/bwzjvvsG/fPgACAwOZNm2a9dilS5d46qmnALBYLKSmpuDq6oabmxu///67XeoVERG5XQ99kM1OAHQEJlMqC0MW0LpVmyzHER5+gjWrV+aYIHvyZDgHDxyg3aPt7V3KA6lPnz7ExcUxb948tm3bZnMsd+7cDB8+HMgIspcvX2TixCnkypXLHqWKiIjckYc+yBoMBlyNrhgM9q7EcaSkpLBw4QK6dHkMDw8Pu9Xh5OSEi9Fot+s/6CpVqgTA5s2bbwiyrq6u1K9fHwCz2cyB/bvZvXs3CxYsuO91ioiI3KmHfo2s0WikW/ceeHg47mzs/ZaSksLCkAUkJyfbtY7ixUvQqlVru9aQUyxcuJiiRYvSrFkze5ciIiKSbTlqRjYpKYnFixfy6KMd2LtnN1HRUVSoUJGgoLI2xzt06MjevXs4d/Yc7R5tzy+//GxdWnC1Tbt27dm/by8RERGUr1CBChUqcvr0KXbu2IG7uzuPNGqMl5eX9dpz587GbDZjdDFStFgx6tSpi7Ozc5bX9fbxIaBUAOXK//unRU+fPsW2rVt5rFv3TMd3s2tkxmw2s3XLZi5evEipgIBM2+zevYvwEyfInScPtWrVwsfn37eWz507y65duzCbzVStUpUSJUsC8OuvSwD4efFC3NzdCQ6uTHBwZQCOhIYSeuQwRqMr1avXoFChQlneg85dumI2m9m2dQuXIy9T1K8oNWvVto7p6preWrVqs3PnDpKTkqhZqxZ+fkWBzJcW7N+/j2NHj+Lj40Ot2nXInTu3TV/VqlVnz55dJCUlUb9+AwoV8mX37l0cP34MP7+i1KtXH8NDNj1vsViYvyCEF1548aEbu4iIOLYcNSObnJzMwpAFDB40gP0H9nPp0iWGDxvC8uVLbY4PGTyI/fv24eTkRFpaGgtDFpCUmHhdm4EcOnSIy5GXGT5sCNO/msakieOJT4hnw4b1DB0yCLPZbL220cWIq9EVU5qJnxcvYtjQwVgsliyvmxAfz08//WBT/+JFCzl3/lyW47vZNTLzyZiPmT37R+KuxLFoYQiLFobYHB/36SdM/2oqcVfi2LxpI/3efIMzZ84AcPjwId55ux+nT58iJiaayZMnsHXL5ow6/nk738WYUY+TU0bw/HbGN4wb9wmRlyM5cSKM9959m0OH/s7yHsTFxfHO22+yYuVyEuITmDNnNkOHDiItLQ3IWNM7b+4cRn/8ERcunOd42HHeebsf586dBTKC/4oVy63jmT9/LhMnfEHclThOnAhj+LAhREZGWvuaO+cnxoweRcTFCA4cOMB7777NpInjWbb0D5KTk/l+5rf8+OOsLO+nyWQiMTHR+pWUlJhlW0eyatUqLly4SM+ePe1dioiIyG3JUTOyV7Vs1YoOHToBUDm4MpMnT6Rx46bW4+07dKR58xYAJCTEZ9rHk08+Rb36DYCMD4Tt2LGNSZOn4ubmRnJyMs8+05NTJ0/iX6oUgM0sao8eT9Dvzb7s2bObatWqZ3rd2NhYFi5cwJkzpylWrDhXrlxh8+ZNDB06IstxZecaV+3atZNDh/5mypdf4ePjg8ViYfjwISQmJACwe9cudu/exeQp08ibNy8An3/2KT/9OIsP+g/gwIH91KhRk1deeQ2A9PR0Ll68CECbNu2YN3cO7dt3tJ77998HWbt2DRMnTbHuK+Vfilnfz2T0mLGZ3oNvZ3xD3rz5GDnyYwwGA917PE7f119hzepVtGrdBoDk5CQGDhpi7XP4sCFs2riRbt173DDmfXv38vgTT9GiRUvrPb52hjE1NZWhw0aQJ08ezGYzL734POnp6Xw4YBAA5cqVZ+qXU+jV65lM7/+ihSHMnz/Xuu3u7sbYsWMzbetIvv32W1q1bEGBAgXsXYqIiMhtyZFBtlat2tbvq9eoSXJyMufOnbOGoSpVqtyyj3LlK1i/L1SwEAGBpXFzcwPA3d0dH59cRMdE409GkI2NiWHHzh1ERUZiSjPh5OTEmTOnbULmtdfNnTs3devWY/nyZTz//AusXbuGQoUK2Sw1uF52rnHVsWNHKVe+Aj4+PkDGh9pq1qjFunV/Wo+XCQqy3hOA2nXq8P3M7zK+r12XVStXMuOb6VQKrkyFChXx8/PLsrZDf/+Nl5cXS5f+++im2NhYwsNP2MwaX3sPjh07Sp06da1h093dncpVqnL02FFrkPXzK2pTY+HChYmJicm0hmbNmhMSsoCYmGjKl69AUFBZ6+wxQJEifuTJkwfI+KBY/vwFKF/B9nWOiYnOcoxdH+tGh46drNsWi5mY6MtZtncE586dY9myZcz87ht7lyIiInLbctTSgquuf7fdYrHYPJXA1dXtln1cv/bU2SmTW/XPdU6fPsXrr7/K7t27SDWlZjwFwclAYoLtW8/XX7dNm3b89edaUlJSWLlyOS1atsqynuxe4yoDhhtuhIWslyEAWMwWIONGlShRgomTplCjZi1OnAjj/fffYdXKFVmfiwVnF2dcja7Wr4IFCvLYY91tlmDc6t5bzGauXaXp7Hzjfc9qHM2at2D06E8oUsSPDevX8UbfVwk/ceKmfTk7Zb3G+HpGoxFPT0/r14P+AcEvv/ySli1bMmPGDMLCwmjZsiWPP/64TZvvvvuOwMBAateuZacqRURE7lyOnJHdtnULnTp3AWD79m14eHji51eUxMR7s6Zx9+7dBAQE8O677wMZn+pftWrlLc8rV748BQoU5KtpX3IpIoImTZpm2fZ2r1EmKIhfflnClStx+PjkwmKxsH37NpvjixcvJDoqirz58gGwectmgoKCAAg7fpziJUpQtWo1qlatRt68eVm2bCktWrayzkybTKnW/ipUqMivv/xC8xYtrTOoFouFo0eOZPmBtDJBQWzbtpUOHTvh5OREYmIie/fu4cknn77lvctMaOhhypYtR4MGDWnQoCFjxnzMxo0brMs/Hjbt2rWjatWqNvtcXV1ttjt16nRDuBUREXEUOTLI/vnnWsLDT+Dq6sqGDet5ts/zuLm53bMgW6FCBWb/9AMTJnxBLh8f9uzdc9MPYV2rdZs2TP9qGg0faWTzxIC7vUbVqtUIDg7mg/ffo0bNmpwIC+NK/BXc/gkyVatWo1btOvTv/x61a9fh9OnTnD17huEjRgEQfjKc8eM/p3yFChhdjGzcuIFH22c8HcDNzY1SpUoxbeqXBJYuTeXKVQgOrkzLVq14951+1KxVGxcXF0IPH6JqteoElS2baY1du3Zj0MD+DBzQn9JlyrBn9y6KFStO02bNs3Xvrrdi+TJmfvctZcoEcSX+Cvv27qHzP7/QPIz8/f3x9/e/aZtKlSphNps5f+7U/SlKRETkP5Qjg+ygQUM4ciSUqOgoRrZpR8A/j57y8PDgySefts4oQsZb3U8++TSeXp5ZtqkUXNn66KmrOnfpQpF/1oyWLl2GcZ+NZ+/e3QC8/35/zpw+jfc/wTSzPq+qXr0mgPUDSlm51TWuHwfAe+/3Z8eO7UREXKRevfp4enpx/Pgx6/G33nqH/fv3cfLkSYKCylK9Rg28vLyBjPWmlSoFs3fvHkwmEwMHDaZ06TLWc4cOG8nWLZuJjY21PrWgZ8/eNGrUhCOhh0lLT6d1qzbW+5bZPcidOzfjPhvPrp07iIyM5Nk+z1OtWnWc/lnGERAQSOs27WzuQ63adaxrakuW9Ld59FbfN/px9OgRQkNDKVGiBD179iZ//vxZ9tWqdRtKXTNbmztPnjueDRYREZH7z2DJ7tShA4iOjuZ/zz/LdzN/cJg/tfnLL0tYuXI5EydO0TM8HczVmczO05NINNm3loOjs/6Q4K1cHUcRvxLWXyIcTU4YA+SMceSEMUDOGEdOGAPknHHIvZEjZ2QdwcWLF1m27A9WrVzJiy++pBArIiIicptyVJC92Vv4DxqDwYC3lzf9+r1NjZo17V2OiIiIiMPJUUHW3d090wflP4gKFSqU5Z+iFREREZFb02ITEREREXFICrIiIiIi4pAUZEVERETEIeWoNbIi9rB5SBlcXPSjJCIicr9pRlZEREREHJKCrIiIiIg4JAVZEREREXFICrIiIiIi4pAUZEVERETEISnIioiIiIhDUpAVEREREYekICsiIiIiDklBVkREREQckoKsiIiIiDgkBVkRERERcUgKsiIiIiLikBRkRURERMQhKciKiIiIiENSkBURERERh6QgKyIiIiIOSUFWRERERBySgqyIiIiIOCQFWRERERFxSAqyIiIiIuKQFGRFRERExCEpyIqIiIiIQ1KQFRERERGHpCArIiIiIg5JQVZEREREHJKCrIiIiIg4JAVZEREREXFICrIiIiIi4pAUZEVERETEIbnYuwARR2WxWAAwm82YzWY7V3PnrtauMdhfThhHThgD5Ixx5IQxwL0Zh8FgwGAw/Gf9if0YLFf/NRaR25KUlERU5EV7lyEiIrepiF8JnJz0pnROoCArcofi4+N5o+8rTJz0JZ6eXvYu544lJSXy4gvPMf3rb/Hw8LR3OXckJ4wBcsY4csIYIGeMIyeMAe7NODQjm3NoaYHIHXJyciI2Ng4nJ2eH/s3eYHAiOTkFg8HJYceRE8YAOWMcOWEMkDPGkRPGADlnHHJv6L8IEREREXFICrIiIiIi4pAUZEXukNFopEePJzAajfYu5a7khHHkhDFAzhhHThgD5Ixx5IQxQM4Zh9wb+rCXiIiIiDgkzciKiIiIiENSkBURERERh6QgKyIiIiIOSc+RFbkDv/32K78sWUxcXBxlygTx0suvUKxYcXuXlSWTycSqVStYuWIFFy6cp2ChQnRo35EWLVtZ2zz7TE+uXLlic9706TPIX6DA/S43S8uXL2X6V9Ns9jVp2oy+fd+0bi9btpTFixYSGxtDYGBpXnzpZUqW9L/PlWbtwIH9DB0y6Ib9gYGlGfvpZwA8/9wzxMTE2ByfOm06hQr53o8SM7VmzWqmTJ5o3R732ReUKhVg02bFiuUsWhhCTEw0AQEBvPjiK/iXKpXt4/fDa6++xIULFwCoUaMmAwYOth5LT09nzZrVLF+2lHPnzpK/QAHatn2Udu0etbZ5+aUXuHQpwqbPCROnUKxYsfszAOCvv/5k4oQvrNufjB1H6dJlrNvXv1YADRo05O133rNur1q1koUh84mOjsbfvxQvvvgyAYGB9774a/Tt+yrnzp4FoGrVagweMsx67Nixo3zw/rs3nFO0aDEmTpoC2L6WV33xxURKlCx574qWB49FRG7Lpo0bLE892cOyZ/duS1xcrOWraVMtL7/0P0tqaqq9S8vSzp07LN98/ZUlPPyEJSkpybJp4wZLj+5dLdu2bbW2eab305b9+/fZscpbW7bsD8uADz/I8vj27dssTz7R3bJz5w5LXFycZcY3X1v+93wfS3Jy8n2s8vakp6dbXnzhecsvv/xs3fdcn96WPXt226+om4iKirJ07dLREhZ23Gb/rp07LU883s2yffs2S1xcnOW7b2dYnuvzjCUxMTFbx++36dOnWUZ9NMJm34ED+y1Tv5xiCQs7bklKSrJs377N8sTj3Szr1v1lbfPSi/+zbN++7X6Xm6nY2FhL1y4dLUePHrHZv3r1Kst7776V5Xl7du+2PPF4N8u2rVsscXFxllnfz7T0ebaXJSEh4V6XnKkZ33xtGTF86C3b9X39Vcv8eXOt26++8qJly5bN97AycQRaWiBym5avWEaTJk2pUrUqPj656P3Ms8TGxrJ79y57l5al6tVr8Pz/XqRkSX/c3d2pV78BZcoEcejQ3/Yu7T+1YvkyGjR8hOrVa+Dj40PPXr1JTk5ix/Zt9i4tSzt37iAuLpYmTZrZu5S7snzFMurXb0DNmrXw8fHh6Z69MJlS2b5ta7aOPwgqVqzEy6+8SqlSAbi7u1OzZi0qVqzEob9z2M/JimXUqVOXWrXr4OPjw1NP98RsNrN16xZ7l5al/fv3ceHCeZq3aGHvUuQBoyArcptOhocTeM3beO7u7hQtWozw8BN2rOr2xMfHc/JkOKX8bd/W/XTsGJ58ojv93uzLqlUr7VTdzYWFHeepJ3vwXJ/ejBs3loiIi9Zj4eEnKF26tHXb1dWV4sVLEB4ebodKs2fF8mXUq1cfHx8fm/2ff/ap9bVYsWK5narLvpPhJ2x+LoxGIyVL+hN+Mjxbxx9ESUlJHD9+/IblD5MmjufJJ7rz5huvsXTp73aq7uZOnTrF0089Tp9nezP2k9GcP3/eeiz8ZLjNUgRnZ2f8/Us90P8PW7F8GTVr1iJfvvw2+7+cMoknn+jOG31f4/fff7VTdWJPWiMrcpsSExPx8vS02efl5UVSYpKdKro9JpOJzz/7lDJlgmjQ8BHr/pnf/whAUlIiu3buZPLkibi4uNCkSVN7lXqD1q3b0rp1WywWCxcuXOD7md8ycsQwPvt8Aq6uriQlJeHp6WVzjpeXF4lJifYp+BYiIiLYs2c3Iz/62Gb/jG+/BzKC1O7du5g8aQIuzs40a/7gzkYlJibhmenPRWK2jj9o0tPTmTDhC/z8/GjWrLl1/7SvvgYgOTmZvXt2M2nSBAwGJ9q0aWuvUm/QrFlzmjVrjsViISIiglnff8eIEUP54ouJuLu7k5SYeMNr4fkAvxaxMTFs27aVDwfYri2f8uVXAKSkpLB37x4mTRyPAQPtHm1vjzLFTjQjK3KbPD09Sbjuf/gJCQl4eHrYqaLsS0lJ4ZNPRpOens77H3yIk9ON/wvw8PCkQcNHaN68JRvWr7NDlbdmMBgoUqQIfd94k/PnzxN2/DgAHh4eJCYm2LRNSEjA08Mzs27sbuXK5RQrXpxy5cpnetzDw4P69RvQsmUr1j+gr8VVnp4eJGb6c+GZreMPEpPJxGeffUpcbAwDBg7GxeXGOR93d3fq1K1Hm7btHuifE19fX/q+0Y/Lly5x5EgoAB6enje8FokP6GsBsHr1KvLnL0CVKlUzPe7m5kbt2nVo1679A/9zIv89BVmR21TS35/jx45at5OTkzl79gz+/vf309e3KykpiVGjRgAwcNAQ3N3d7VzRf8/fvxTHjh2zbptMJk6fPoW/v7/9ispCWloaa1avplWr1vYu5T9R0r+Uzc+FyWTi1KmT+P/zxIhbHX9QpKSk8MmYj4mPv8LgIcPx8vK69UkPOIPBYLPtX9KfY9e8Funp6YSHn3gg/x9mNptZuWoFLVu2umEcIqAgK3LbWrdqw59/rmXv3j1cuXKFWd/PJFeu3FSrVt3epWUpPj6e4cOG4OPtwwcffIirq6vN8Z07drAwZAEREREkJSWxadNGVq9eSf0GDe1UceamTp3C/v37SEpK4sKFC0yeNBHfwoUpFZDxGKhWrduwccN6du3aSXx8PD/M+j7jQzu1atu58htt27aV5OQkGje2Xbqxe/cuFsyfR0TERZKSkti8eROrVq2kwQP2WlyvVavWbNq0kZ07dhAfH8/sn37ExcVIrdp1snX8QZCUlMioj0ZgMBgYNGgoHh6277Ls37+PuXNnc+HCBZKTk9m2bSvLly194H5Opn81jb1795CUlEhExEUmT5pA/vwFKFMmCIBWrdqwdesWtm/ban0tnJycqFOnrp0rv9GePbuJioy8YVnNwYMHmT37Ry5cOE9ycjLbt2/jjz9+f+B/TuS/Z7BYLBZ7FyHiaH779ReWLPmZuLhYygQF8dJLr1C8eAl7l5WlzJ4rCdCoUWPe7Pc2qamp/LLkZ9asWU1sbAy+voVp2+5RWl7znNkHQVhYGLN/+oHDhw/h7u5OhYqVeOqpnhQuXNjaZtnSP1i8eCExMVefI/vKAzkjO2zYYHwL+fLKq6/b7DeZTPz6yxJWr15JTEzGa9GmTVtatW5jp0oznDgRxrvvvHXD/kGDhlKtesYvcSuWL2Oh9Tmxgbz40ss2z5q91fH74dOxY9iyZbPNvsDAQMZ++jmbN21k3LixN5xTq3Yd+vcfgMlk4vfffmXVqpVERUXi61uYlq1a2zxn9n44dfIkb731xg37PxwwiJo1axEeHs7sn37g0KG/cXV1o3z58jz1dE/8/Ipa265auYKQkAVER0dRqlSAXZ4j+/lnn7Jx4wabff7+pfjs8/HW7TFjPsbV1ZW337Z9pmxaWhp//P4bK1cuJzIykkKFfGnZqhXt2rXXzO1DRkFWRERERBySlhaIiIiIiENSkBURERERh6QgKyIiIiIOSUFWRERERBySgqyIiIiIOCQFWRERERFxSAqyIiIiIuKQFGRFRERExCEpyIqIiIiIQ1KQFRERERGHpCArIiIiIg5JQVZEREREHNL/ARK3Yq+8rE8yAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from collections import Counter\n", + "from src.data.multimodal import clean_kegg_answer\n", + "answers = Counter(clean_kegg_answer(a) for a in DS[\"kegg\"][\"answer\"])\n", + "print(f\"{len(answers)} distinct KEGG answers; top 15 cover {sum(c for _, c in answers.most_common(15)) / len(DS['kegg']):.0%}\")\n", + "H.plot_counts(dict(answers.most_common(15)), \"KEGG train: most frequent answers (after clean_kegg_answer)\");" + ] + }, + { + "cell_type": "markdown", + "id": "cell-017", + "metadata": {}, + "source": [ + "## 5. `datasets_config.json`: where datasets are registered\n", + "\n", + "Every dataset PRISM can stream is an entry in `src/data/datasets_config.json`. Here is the KEGG entry:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "cell-018", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "`src/data/datasets_config.json:394-415`\n", + "```json\n", + " 394 \"dna_bioreason\": {\n", + " 395 \"name\": \"DNA sequences\",\n", + " 396 \"hf_id\": \"wanglab/kegg\",\n", + " 397 \"split\": \"train\",\n", + " 398 \"weight\": 0.50,\n", + " 399 \"handler\": \"dna_bioreason\",\n", + " 400 \"modality\": \"dna\",\n", + " 401 \"local_path\": \"${PRISM_DATA_ROOT}/dna_local_shards/kegg\",\n", + " 402 \"preferred_source\": \"local\",\n", + " 403 \"required_keys\": [\n", + " 404 \"question\",\n", + " 405 \"answer\",\n", + " 406 \"reasoning\",\n", + " 407 \"reference_sequence\",\n", + " 408 \"variant_sequence\"\n", + " 409 ],\n", + " 410 \"fallback_dummy\": false,\n", + " 411 \"skip\": false,\n", + " 412 \"size_formatted\": \"1,159 samples (train split)\"\n", + " 413 },\n", + " 414 \"dna_bioreason_variant_effect_coding\": {\n", + " 415 \"name\": \"Variant Effect (Coding)\",\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.show_source(\"src/data/datasets_config.json\", pattern=r'\"dna_bioreason\": \\{', context=22)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-019", + "metadata": {}, + "source": [ + "| Field | Meaning |\n", + "|---|---|\n", + "| `handler` | Picks the `_process_` method in `src/data/multimodal.py` that turns a raw row into a chat prompt + DNA (section 6). |\n", + "| `modality` | The modality group it is sampled under (`dna`). Sampling first picks a modality, then a dataset within it by `weight`. |\n", + "| `local_path` / `preferred_source` | `local` loads the Arrow files from section 3. `${PRISM_DATA_ROOT}` is expanded from the environment. |\n", + "| `hf_id` | Hub fallback, and the id that `training.bioreason_dataset` refers to. |\n", + "| `required_keys` | Columns that must exist. A dataset missing one fails loudly at load time. |\n", + "| `skip` | `true` excludes the dataset. Training presets flip this per run instead of editing the JSON. |\n", + "\n", + "**Per-run overrides.** Each BioReason training preset composes `src/conf/data/bioreason_kegg_only.yaml`, which patches entries without touching the JSON:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cell-020", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "`src/conf/data/bioreason_kegg_only.yaml:1-16`\n", + "```yaml\n", + " 1 # BioReason KEGG-only data mix (wanglab/kegg via the dna_bioreason handler).\n", + " 2 # Pins every zone_a dataset that is default-on in datasets_config.json off, so\n", + " 3 # the SFT run cannot silently mix in another stream (ts_qa is skip=false there).\n", + " 4 #\n", + " 5 # Composed automatically by training=bioreason_{projector,sft,grpo} via their\n", + " 6 # defaults list (/data@_global_.data) -- do not also pass +data=bioreason_kegg_only.\n", + " 7 dataset_overrides:\n", + " 8 dna_bioreason:\n", + " 9 skip: false\n", + " 10 preferred_source: local\n", + " 11 dna_bioreason_variant_effect_coding:\n", + " 12 skip: true\n", + " 13 dna_bioreason_variant_effect_non_snv:\n", + " 14 skip: true\n", + " 15 ts_qa:\n", + " 16 skip: true\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.show_source(\"src/conf/data/bioreason_kegg_only.yaml\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-021", + "metadata": {}, + "source": [ + "### Adding data: a KEGG + variant-effect mix\n", + "\n", + "The original BioReason recipe mixes KEGG with the variant-effect sets (weights 0.5 / 0.25 / 0.25). The variant-effect entries already exist in the JSON, so a **new data YAML** that turns them on and points them at our downloads is all it takes. Passing `data=bioreason_demo_mix` on the command line swaps it in for `bioreason_kegg_only`.\n", + "\n", + "Because it *replaces* `bioreason_kegg_only`, it has to pin every default-on stream itself (`ts_qa`). `${oc.env:PRISM_DATA_ROOT}` is resolved when the job starts." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "cell-022", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Written by applications/bioreason/bioreason_demo.ipynb (section 5).\n", + "# KEGG + variant-effect mix (BioReason weights). Use with: data=bioreason_demo_mix\n", + "dataset_overrides:\n", + " dna_bioreason:\n", + " skip: false\n", + " preferred_source: local\n", + " weight: 0.5\n", + " dna_bioreason_variant_effect_coding:\n", + " skip: false\n", + " preferred_source: local\n", + " weight: 0.25\n", + " local_path: ${oc.env:PRISM_DATA_ROOT}/dna_local_shards/variant_effect_coding\n", + " dna_bioreason_variant_effect_non_snv:\n", + " skip: false\n", + " preferred_source: local\n", + " weight: 0.25\n", + " local_path: ${oc.env:PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv\n", + " ts_qa:\n", + " skip: true\n", + "\n" + ] + } + ], + "source": [ + "MIX_YAML = REPO / \"src/conf/data/bioreason_demo_mix.yaml\"\n", + "MIX_YAML.write_text('''\\\n", + "# Written by applications/bioreason/bioreason_demo.ipynb (section 5).\n", + "# KEGG + variant-effect mix (BioReason weights). Use with: data=bioreason_demo_mix\n", + "dataset_overrides:\n", + " dna_bioreason:\n", + " skip: false\n", + " preferred_source: local\n", + " weight: 0.5\n", + " dna_bioreason_variant_effect_coding:\n", + " skip: false\n", + " preferred_source: local\n", + " weight: 0.25\n", + " local_path: ${oc.env:PRISM_DATA_ROOT}/dna_local_shards/variant_effect_coding\n", + " dna_bioreason_variant_effect_non_snv:\n", + " skip: false\n", + " preferred_source: local\n", + " weight: 0.25\n", + " local_path: ${oc.env:PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv\n", + " ts_qa:\n", + " skip: true\n", + "''')\n", + "print(MIX_YAML.read_text())" + ] + }, + { + "cell_type": "markdown", + "id": "cell-023", + "metadata": {}, + "source": [ + "Validate the new mix the way the loader will: resolve the overrides, check each `local_path` exists, and check its Arrow columns against `required_keys`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "cell-024", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "W1011 23:25:11.983000 1232379 site-packages/torch/utils/_pytree.py:630] is an Enum subclass and is now natively supported by torch.compile as an opaque value type. Calling register_constant() on Enum subclasses is deprecated and will be an error in a future release.\n", + "W1011 23:25:12.007000 1232379 site-packages/torch/utils/_pytree.py:630] is an Enum subclass and is now natively supported by torch.compile as an opaque value type. Calling register_constant() on Enum subclasses is deprecated and will be an error in a future release.\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
weightlocal_patharrow foundmissing required_keys
dataset
dna_bioreason0.50/lus/flare/projects/ModCon/abalaji/cache/dna_l...True[]
dna_bioreason_variant_effect_coding0.25/lus/flare/projects/ModCon/abalaji/cache/dna_l...True[]
dna_bioreason_variant_effect_non_snv0.25/lus/flare/projects/ModCon/abalaji/cache/dna_l...True[]
\n", + "
" + ], + "text/plain": [ + " weight \\\n", + "dataset \n", + "dna_bioreason 0.50 \n", + "dna_bioreason_variant_effect_coding 0.25 \n", + "dna_bioreason_variant_effect_non_snv 0.25 \n", + "\n", + " local_path \\\n", + "dataset \n", + "dna_bioreason /lus/flare/projects/ModCon/abalaji/cache/dna_l... \n", + "dna_bioreason_variant_effect_coding /lus/flare/projects/ModCon/abalaji/cache/dna_l... \n", + "dna_bioreason_variant_effect_non_snv /lus/flare/projects/ModCon/abalaji/cache/dna_l... \n", + "\n", + " arrow found missing required_keys \n", + "dataset \n", + "dna_bioreason True [] \n", + "dna_bioreason_variant_effect_coding True [] \n", + "dna_bioreason_variant_effect_non_snv True [] " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import glob\n", + "from hydra import initialize_config_dir, compose\n", + "from hydra.core.global_hydra import GlobalHydra\n", + "import pandas as pd\n", + "import src.train as T\n", + "from src.data.dataset_manager import DatasetManager\n", + "\n", + "def compose_cfg(*overrides):\n", + " # Same composition as `python src/train.py `.\n", + " GlobalHydra.instance().clear()\n", + " with initialize_config_dir(config_dir=str(REPO / \"src/conf\"), version_base=None):\n", + " return compose(config_name=\"config\", overrides=list(overrides))\n", + "\n", + "cfg_mix = compose_cfg(\"model=prism_olmo_1b\", \"training=bioreason_sft\",\n", + " \"training.resume_weights_only=null\", \"data=bioreason_demo_mix\")\n", + "overrides = T._resolve_dataset_overrides(cfg_mix)\n", + "entries = DatasetManager().get_zone_config(\"zone_a\")[\"datasets\"]\n", + "\n", + "rows = []\n", + "for name, ov in overrides.items():\n", + " e = {**entries[name], **ov}\n", + " if e.get(\"skip\"):\n", + " continue\n", + " path = os.path.expandvars(e[\"local_path\"])\n", + " cols = set(load_from_disk(path).column_names) if glob.glob(f\"{path}/*.arrow\") else set()\n", + " rows.append({\"dataset\": name, \"weight\": e[\"weight\"], \"local_path\": path,\n", + " \"arrow found\": bool(cols), \"missing required_keys\": sorted(set(e[\"required_keys\"]) - cols)})\n", + "display(pd.DataFrame(rows).set_index(\"dataset\"))" + ] + }, + { + "cell_type": "markdown", + "id": "cell-025", + "metadata": {}, + "source": [ + "
Registering a new dataset, rather than enabling an existing one\n", + "\n", + "Add an entry under `zone_a.datasets` in `src/data/datasets_config.json` with the fields above. Reuse an existing `handler` if the row schema matches (e.g. `dna_bioreason_variant_effect_coding` for `question/answer/reference_sequence/variant_sequence`). Otherwise, add a `_process_` method plus a branch in the handler dispatch (section 6). Then run `pytest tests/data/test_dna_bioreason_datasets_config.py`, which pins the DNA entries' schema.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "cell-026", + "metadata": {}, + "source": [ + "## 6. How the DNA modality is registered\n", + "\n", + "PRISM has no plugin registry. A modality is wired in through a handful of explicit touch points, and following DNA through them is the best map for adding your own.\n", + "\n", + "**① The enum.** Every modality is a member of `Modality`. `ModelConfig.modalities` (`[text, dna]` in `prism_olmo_1b.yaml`) selects which ones a model builds." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-027", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "`src/modalities.py:23-36`\n", + "```python\n", + " 23 class Modality(str, Enum):\n", + " 24 \"\"\"The modality identifiers PRISM recognizes.\n", + " 25 \n", + " 26 Members are the canonical lowercase strings used as keys throughout the\n", + " 27 framework: ``config.modalities``, the ``encoders``/``projectors``\n", + " 28 ModuleDicts on ``UnifiedTransformer``, batch example dicts, and the\n", + " 29 ``modality`` field in ``datasets_config.json``.\n", + " 30 \n", + " 31 Because the enum subclasses ``str``, a member compares equal to and\n", + " 32 hashes like its value, so ``Modality.IMAGE == \"image\"`` is True and\n", + " 33 ``d[Modality.IMAGE]`` and ``d[\"image\"]`` reach the same entry. The\n", + " 34 explicit ``__str__`` below -- not the ``str`` base -- is what makes\n", + " 35 ``str(Modality.IMAGE)`` render as ``\"image\"`` instead of\n", + " 36 ``\"Modality.IMAGE\"``; removing it would change f-string and log output.\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.show_source(\"src/modalities.py\", pattern=r\"^class Modality\", context=30)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-028", + "metadata": {}, + "source": [ + "**② The encoder** subclasses `ModalityEncoder`. `DNAEncoder` loads any HF masked-LM checkpoint, probes its hidden size, and adds a `Linear` when that differs from `d_dna`. `tokens_per_instance()` is the fixed number of embeddings one DNA span becomes." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "cell-029", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "`src/encoders/dna.py:14-58`\n", + "```python\n", + " 14 class DNAEncoder(ModalityEncoder):\n", + " 15 \"\"\"\n", + " 16 DNA sequence encoder using Nucleotide Transformer (NT) or Evo2.\n", + " 17 \n", + " 18 Supports:\n", + " 19 - InstaDeepAI/nucleotide-transformer-v2-500m-multi-species (default)\n", + " 20 - togethercomputer/evo2_1b_base (set dna_is_evo2=True)\n", + " 21 \n", + " 22 Input: Tokenized DNA sequences (B, T) as input_ids\n", + " 23 Output: Sequence features (B, T, d_dna)\n", + " 24 \"\"\"\n", + " 25 \n", + " 26 def __init__(\n", + " 27 self,\n", + " 28 d_dna: int = 1024,\n", + " 29 model_name: str = \"InstaDeepAI/nucleotide-transformer-v2-500m-multi-species\",\n", + " 30 dna_is_evo2: bool = False,\n", + " 31 max_dna_length: int | None = None,\n", + " 32 ):\n", + " 33 super().__init__(d_dna)\n", + " 34 self.model_name = model_name\n", + " 35 self._max_dna_length_override = max_dna_length\n", + " 36 \n", + " 37 logger.info(f\"Loading DNA Encoder: {model_name}...\")\n", + " 38 self.model = AutoModelForMaskedLM.from_pretrained(model_name, trust_remote_code=True)\n", + " 39 \n", + " 40 # Probe the actual hidden state dimension by requesting output_hidden_states.\n", + " 41 # AutoModelForMaskedLM.outputs[0] = LM-head logits (vocab_size ~4096), NOT hidden\n", + " 42 # states. Using hidden_states[-1] gives the last encoder layer's representation,\n", + " 43 # which is what downstream tasks want.\n", + " 44 with torch.no_grad():\n", + " 45 dummy_ids = torch.zeros(1, 6, dtype=torch.long)\n", + " 46 try:\n", + " 47 dummy_out = self.model(dummy_ids, output_hidden_states=True)\n", + " 48 if (\n", + " 49 hasattr(dummy_out, \"hidden_states\")\n", + " 50 and dummy_out.hidden_states is not None\n", + " 51 and len(dummy_out.hidden_states) > 0\n", + " 52 ):\n", + " 53 model_hidden_size = dummy_out.hidden_states[-1].shape[-1]\n", + " 54 logger.info(f\"DNA model hidden_states[-1] dim (probed): {model_hidden_size}\")\n", + " 55 elif hasattr(dummy_out, \"last_hidden_state\") and dummy_out.last_hidden_state is not None:\n", + " 56 model_hidden_size = dummy_out.last_hidden_state.shape[-1]\n", + " 57 logger.info(f\"DNA model last_hidden_state dim (probed): {model_hidden_size}\")\n", + " 58 else:\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.show_source(\"src/encoders/dna.py\", pattern=r\"class DNAEncoder\", context=45)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-030", + "metadata": {}, + "source": [ + "**③ Model wiring.** `UnifiedTransformer.__init__` builds the encoder and its projector when the modality is enabled:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "cell-031", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "`src/model.py:590-623`\n", + "```python\n", + " 590 if Modality.DNA in config.modalities:\n", + " 591 dna_model_name = getattr(\n", + " 592 config,\n", + " 593 \"dna_model_name\",\n", + " 594 \"InstaDeepAI/nucleotide-transformer-2.5b-multi-species\",\n", + " 595 )\n", + " 596 dna_is_evo2 = getattr(config, \"dna_is_evo2\", False)\n", + " 597 \n", + " 598 self.encoders[\"dna\"] = DNAEncoder(\n", + " 599 d_dna=config.d_dna,\n", + " 600 model_name=dna_model_name,\n", + " 601 dna_is_evo2=dna_is_evo2,\n", + " 602 max_dna_length=getattr(config, \"max_dna_length\", None),\n", + " 603 )\n", + " 604 # Output dimension from the encoder (d_dna after its internal projection)\n", + " 605 dna_input_dim = getattr(self.encoders[\"dna\"], \"hidden_dim\", config.d_dna)\n", + " 606 \n", + " 607 self.projectors[\"dna\"] = ModalityProjector(\n", + " 608 dna_input_dim,\n", + " 609 target_dim,\n", + " 610 norm_mode=proj_norm_mode,\n", + " 611 target_norm=proj_target_norm,\n", + " 612 modality_embed_pos=proj_embed_pos,\n", + " 613 modality_embed_scale=proj_embed_scale,\n", + " 614 text_norm_mean=proj_text_norm_mean,\n", + " 615 text_norm_std=proj_text_norm_std,\n", + " 616 text_elem_mean=proj_text_elem_mean,\n", + " 617 text_elem_std=proj_text_elem_std,\n", + " 618 norm_clip_min=proj_norm_clip_min,\n", + " 619 norm_clip_max=proj_norm_clip_max,\n", + " 620 hidden_mult=proj_hidden_mult,\n", + " 621 num_layers=proj_num_layers,\n", + " 622 )\n", + " 623 # === Output decoders (Phase 1) ===\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.show_source(\"src/model.py\", pattern=r\"if Modality.DNA in config.modalities\", context=34)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-032", + "metadata": {}, + "source": [ + "**④ Data.** The `handler` string from `datasets_config.json` is dispatched to a processor:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "cell-033", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "`src/data/multimodal.py:3530-3537`\n", + "```python\n", + " 3530 # DNA Handlers\n", + " 3531 elif handler_key == \"dna_bioreason\":\n", + " 3532 processor = self._process_dna_bioreason\n", + " 3533 elif handler_key == \"dna_bioreason_variant_effect_coding\":\n", + " 3534 processor = self._process_dna_bioreason_variant_effect_coding\n", + " 3535 elif handler_key == \"dna_bioreason_variant_effect_non_snv\":\n", + " 3536 processor = self._process_dna_bioreason_variant_effect_non_snv\n", + " 3537 \n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "`src/data/multimodal.py:2888-2947`\n", + "```python\n", + " 2888 def _process_dna_bioreason(self, item):\n", + " 2889 \"\"\"\n", + " 2890 Format a KEGG BioReason example into the structured chat dict consumed by\n", + " 2891 qwen_dna_collate_fn. Mirrors _format_kegg in BioReason/bioreason/dataset/kegg.py.\n", + " 2892 \n", + " 2893 model_name=\"llm\" — DNA sequences are inlined as plain text; dna_sequences is empty.\n", + " 2894 model_name=\"dna-llm\" — DNA sequences are passed as a separate modality; text is question-only.\n", + " 2895 \n", + " 2896 is_sft=True — assistant turn (reasoning + answer) is appended; collate fn masks prompt tokens.\n", + " 2897 is_sft=False — assistant turn is omitted; answer is kept for reward computation (RL/GRPO).\n", + " 2898 \n", + " 2899 Returns a dict:\n", + " 2900 {\n", + " 2901 \"prompt\": list[dict], # chat-format messages\n", + " 2902 \"dna_sequences\": list[str], # [\"ref\", \"var\"] or [\"\", \"\"] for llm mode\n", + " 2903 \"answer\": str, # ground-truth answer\n", + " 2904 }\n", + " 2905 \"\"\"\n", + " 2906 if self.model_name not in (\"llm\", \"dna-llm\"):\n", + " 2907 raise ValueError(f\"Unsupported model_name for DNA BioReason: {self.model_name!r}\")\n", + " 2908 \n", + " 2909 # --- fallback for None / corrupt items ---\n", + " 2910 if item is None:\n", + " 2911 question = \"What disease does this DNA sequence exhibit?\"\n", + " 2912 ref, var = (\"\", \"\") if self.model_name == \"llm\" else (\"CTGA\", \"CTGA\")\n", + " 2913 if self.model_name == \"llm\":\n", + " 2914 question_text = f\"Reference sequence: CTGA\\nVariant sequence: CTGA\\nQuestion: {question}\"\n", + " 2915 else:\n", + " 2916 question_text = question\n", + " 2917 \n", + " 2918 fallback_prompt = [\n", + " 2919 {\n", + " 2920 \"role\": \"user\",\n", + " 2921 \"content\": [\n", + " 2922 {\"type\": \"dna_reference\", \"text\": None},\n", + " 2923 {\"type\": \"dna_variant\", \"text\": None},\n", + " 2924 {\"type\": \"text\", \"text\": question_text},\n", + " 2925 ],\n", + " 2926 }\n", + " 2927 ]\n", + " 2928 if self.is_sft:\n", + " 2929 fallback_prompt.append({\n", + " 2930 \"role\": \"assistant\",\n", + " 2931 \"reasoning_content\": \"\",\n", + " 2932 \"content\": [{\"type\": \"text\", \"text\": \"No DNA sequence information available.\"}],\n", + " 2933 })\n", + " 2934 return {\n", + " 2935 \"prompt\": fallback_prompt,\n", + " 2936 \"dna_sequences\": [ref, var],\n", + " 2937 \"answer\": \"No DNA sequence information available.\",\n", + " 2938 \"class_weight\": torch.tensor(1.0, dtype=torch.float32),\n", + " 2939 }\n", + " 2940 \n", + " 2941 try:\n", + " 2942 ref = item[\"reference_sequence\"]\n", + " 2943 var = item[\"variant_sequence\"]\n", + " 2944 question = item.get(\"question\", \"What disease does this DNA sequence exhibit?\")\n", + " 2945 answer = clean_kegg_answer(item[\"answer\"])\n", + " 2946 reasoning = item.get(\"reasoning\", \"\")\n", + " 2947 \n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.show_source(\"src/data/multimodal.py\", pattern=r'# DNA Handlers', context=8)\n", + "H.show_source(\"src/data/multimodal.py\", pattern=r\"def _process_dna_bioreason\\(self, item\\)\", context=60)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-034", + "metadata": {}, + "source": [ + "**Checklist for a new modality**, using DNA as the template:\n", + "\n", + "| # | File | What to add |\n", + "|---|---|---|\n", + "| 1 | `src/modalities.py` | enum member + a `make_dummy_batch` branch |\n", + "| 2 | `src/encoders/.py` | `ModalityEncoder` subclass; `forward → (B, T, D)`; `tokens_per_instance()` |\n", + "| 3 | `src/config.py` | `d_`, `_model_name`, length fields on `ModelConfig` |\n", + "| 4 | `src/train.py` → `build_model_config` | pass the new `cfg.model.*` fields through (fields are explicit keyword arguments, so a new YAML key is ignored until it's wired here) |\n", + "| 5 | `src/model.py` | `if Modality.X in config.modalities:` encoder + `ModalityProjector` |\n", + "| 6 | `src/data/multimodal.py` | key in `modality_groups`; handler dispatch + `_process_`; an `elif modality is Modality.X` branch that renders tags and fills `example[\"\"]` |\n", + "| 7 | `src/data/datasets_config.json` | the dataset entry (section 5) |\n", + "| 8 | `src/conf/model/.yaml` | `modalities`, `d_`, `is_interleaved_qa: true`, `modality_start_end_token_indices` (section 8) |\n", + "\n", + "The full playbook is in `docs/skills/prism-adding-a-modality/SKILL.md`." + ] + }, + { + "cell_type": "markdown", + "id": "cell-035", + "metadata": {}, + "source": [ + "## 7. Pointing at the DNA encoder\n", + "\n", + "The encoder is a model-config field, `model.dna_model_name`, and it accepts a Hub id or a local path. `prism_olmo_1b.yaml` doesn't set it, so it falls back to `InstaDeepAI/nucleotide-transformer-2.5b-multi-species`. Here we point it **explicitly at the local snapshot** we downloaded. The leading `+` adds a key the YAML doesn't have." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "cell-036", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "backbone allenai/OLMo-1B-0724-hf\n", + "dna_model_name /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "d_dna 768 | max_dna_length 1024 | modalities ['text', 'dna']\n" + ] + } + ], + "source": [ + "DNA_ENCODER = SNAPSHOTS[\"dna\"]\n", + "BASE = [\"model=prism_olmo_1b\", f\"+model.dna_model_name={DNA_ENCODER}\"]\n", + "\n", + "cfg = compose_cfg(*BASE, \"training=bioreason_projector\")\n", + "mc = T.build_model_config(cfg) # exactly what src/train.py builds\n", + "print(\"backbone \", mc.llm_backbone_id)\n", + "print(\"dna_model_name \", mc.dna_model_name)\n", + "print(\"d_dna \", mc.d_dna, \"| max_dna_length\", mc.max_dna_length, \"| modalities\", [m.value for m in mc.modalities])" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "cell-037", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-10-11 23:25:27,060 - src.encoders.dna - INFO - Loading DNA Encoder: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0e9b48f2bd8e424881358d35f908bcac", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading weights: 0%| | 0/524 [00:00 768\n", + "2026-10-11 23:26:08,415 - src.encoders.dna - INFO - DNA Encoder loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NT hidden size 2560\n", + "added projection Linear(in_features=2560, out_features=768, bias=True)\n", + "tokens_per_instance 1024\n", + "4,001 bp -> input_ids (1, 1024) -> embeddings (1, 1024, 768)\n" + ] + } + ], + "source": [ + "from transformers import AutoTokenizer\n", + "from datasets import load_from_disk\n", + "from src.encoders.dna import DNAEncoder\n", + "dev = torch.device(\"xpu:0\")\n", + "\n", + "# The DNA tokenizer comes from the SAME checkpoint as the encoder (what the data pipeline does).\n", + "dna_tok = AutoTokenizer.from_pretrained(mc.dna_model_name, trust_remote_code=True)\n", + "kegg = load_from_disk(str(SHARDS / \"kegg\"))\n", + "\n", + "enc = DNAEncoder(d_dna=mc.d_dna, model_name=mc.dna_model_name, max_dna_length=mc.max_dna_length).to(dev).eval()\n", + "seq = kegg[0][\"reference_sequence\"]\n", + "ids = dna_tok(seq, return_tensors=\"pt\", padding=\"max_length\", truncation=True, max_length=mc.max_dna_length)\n", + "with torch.no_grad():\n", + " out = enc({k: v.to(dev) for k, v in ids.items()})\n", + "print(\"NT hidden size \", enc.model.config.hidden_size)\n", + "print(\"added projection \", getattr(enc, \"proj\", None))\n", + "print(\"tokens_per_instance \", enc.tokens_per_instance())\n", + "print(f\"{len(seq):,} bp -> input_ids {tuple(ids['input_ids'].shape)} -> embeddings {tuple(out.shape)}\")\n", + "del enc, out; torch.xpu.empty_cache()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-038", + "metadata": {}, + "source": [ + "### Why the DNA tokenizer must come from the same checkpoint\n", + "\n", + "The data pipeline used to hard-code the **NT-v2-250m** tokenizer while the encoder was **NT-2.5B (v1)**. Both are \"6-mer\" vocabularies of the same size, but they **order the 6-mers differently**, so the encoder was silently fed the wrong k-mers. `StreamingMultimodalDataset` now loads the DNA tokenizer from `model.dna_model_name`. Here is the mismatch:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "cell-039", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NT-2.5B (v1) idNT-v2 id
AAAAAT57
ACGTAC726728
GGGCCC40784080
TTTTTT13691371
\n", + "
" + ], + "text/plain": [ + " NT-2.5B (v1) id NT-v2 id\n", + "AAAAAT 5 7\n", + "ACGTAC 726 728\n", + "GGGCCC 4078 4080\n", + "TTTTTT 1369 1371" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6-mers with a different id: 4,096 of 4,097\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "def nt_vocab(snapshot):\n", + " # Token order of an NT checkpoint, read straight from its vocab.txt (line i = id i).\n", + " return {tok: i for i, tok in enumerate(Path(snapshot, \"vocab.txt\").read_text().split())}\n", + "\n", + "v2_vocab = next(Path(HF_HOME, \"hub\").glob(\"models--InstaDeepAI--nucleotide-transformer-v2-250m-multi-species/snapshots/*/vocab.txt\"), None)\n", + "v2_path = v2_vocab.parent if v2_vocab else None\n", + "if v2_path:\n", + " v1v, v2v = nt_vocab(SNAPSHOTS[\"dna\"]), nt_vocab(v2_path)\n", + " kmers = [\"AAAAAT\", \"ACGTAC\", \"GGGCCC\", \"TTTTTT\"]\n", + " display(pd.DataFrame({\"NT-2.5B (v1) id\": [v1v[k] for k in kmers], \"NT-v2 id\": [v2v[k] for k in kmers]}, index=kmers))\n", + " print(f\"6-mers with a different id: {sum(v1v[k] != v2v.get(k) for k in v1v if len(k) == 6):,} of {sum(len(k) == 6 for k in v1v):,}\")\n", + "else:\n", + " print(\"NT-v2 not cached; skip. (Also: NT-v2's remote code doesn't import under transformers 5, so it can't be the encoder on frameworks/2026.1.0.)\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-040", + "metadata": {}, + "source": [ + "## 8. Special tokens\n", + "\n", + "DNA enters the text stream through **placeholder pairs**, declared in the model YAML:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "cell-041", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "`src/conf/model/prism_olmo_1b.yaml:65-68`\n", + "```yaml\n", + " 65 is_interleaved_qa: true\n", + " 66 modality_start_end_token_indices:\n", + " 67 dna_reference: [\"\", \"\"]\n", + " 68 dna_variant: [\"\", \"\"]\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "tag raw OLMo tokenizer (Hub) tokenizers/prism-olmo-1b-interleaved\n", + " ['<', 'd', 'na', '_', 'ref', '_', 'start', '>'] [''] id=50292\n", + " ['<', 'd', 'na', '_', 'ref', '_', 'end', '>'] [''] id=50293\n", + " ['<', 'd', 'na', '_', 'var', '_', 'start', '>'] [''] id=50294\n", + " ['<', 'd', 'na', '_', 'var', '_', 'end', '>'] [''] id=50295\n" + ] + } + ], + "source": [ + "H.show_source(\"src/conf/model/prism_olmo_1b.yaml\", pattern=r\"^is_interleaved_qa\", context=4)\n", + "TAGS = [t for pair in mc.modality_start_end_token_indices.values() for t in pair]\n", + "llm_tok = AutoTokenizer.from_pretrained(\"tokenizers/prism-olmo-1b-interleaved\") # model YAML tokenizer_id\n", + "raw_tok = AutoTokenizer.from_pretrained(SNAPSHOTS[\"llm\"]) # the Hub OLMo tokenizer\n", + "print(f\"{'tag':16s} {'raw OLMo tokenizer (Hub)':42s} tokenizers/prism-olmo-1b-interleaved\")\n", + "for t in TAGS:\n", + " print(f\"{t:16s} {str(raw_tok.tokenize(t)):42s} {llm_tok.tokenize(t)} id={llm_tok.convert_tokens_to_ids(t)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-042", + "metadata": {}, + "source": [ + "The Hub tokenizer splits each tag into sub-words. PRISM's `tokenizers/prism-olmo-1b-interleaved` (the model YAML's `tokenizer_id`) **ships with the four tags pre-registered** (ids 50292–50295, inside OLMo's padded 50,304-row embedding table).\n", + "\n", + "`UnifiedTransformer.__init__` handles any tag still missing from the vocab: it calls `add_special_tokens`, resizes the embedding table (seeded so every rank initializes the new rows identically), and then **replaces the strings with integer ids in `config.modality_start_end_token_indices`, in place**. Here that path is a no-op, because the tags are already in the vocab." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "cell-043", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "`src/model.py:414-435`\n", + "```python\n", + " 414 {\"additional_special_tokens\": missing}\n", + " 415 )\n", + " 416 if num_added > 0:\n", + " 417 rng_state = torch.get_rng_state()\n", + " 418 try:\n", + " 419 torch.manual_seed(0)\n", + " 420 self.backbone.resize_token_embeddings(\n", + " 421 len(self.backbone_tokenizer)\n", + " 422 )\n", + " 423 finally:\n", + " 424 torch.set_rng_state(rng_state)\n", + " 425 logger.info(\n", + " 426 f\"Resized token embeddings to {len(self.backbone_tokenizer)} \"\n", + " 427 f\"after adding {num_added} modality special token(s)\"\n", + " 428 )\n", + " 429 \n", + " 430 resolved = dict(config.modality_start_end_token_indices)\n", + " 431 for modality, tags in unresolved.items():\n", + " 432 resolved[modality] = tuple(\n", + " 433 self.backbone_tokenizer.convert_tokens_to_ids(tok)\n", + " 434 if isinstance(tok, str)\n", + " 435 else tok\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.show_source(\"src/model.py\", pattern=r\"additional_special_tokens\", context=22)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-044", + "metadata": {}, + "source": [ + "Two consequences, both of which once broke training silently:\n", + "- `train.py` **adopts `model.backbone_tokenizer`** for the data pipeline. A tokenizer without the tags splits `` into sub-words, so the splice never matches.\n", + "- Because the config is rewritten in place, and `train.py` builds the dataset *after* the model, the prompt renderer used to see `(50292, 50293)` and write the literal text `Reference sequence: 5029250293`. No tag token ever reached the model, so DNA was dropped and every stage trained text-only, with no error. `_render_dna_prompt_text` now maps ids back to token strings. Section 10 asserts that the splice happens." + ] + }, + { + "cell_type": "markdown", + "id": "cell-045", + "metadata": {}, + "source": [ + "## 9. Model build: inspect, then modify\n", + "\n", + "### 9a. The stage-1 (projector) model as shipped" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "cell-046", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-10-11 23:26:16,416 - src.model - INFO - Initializing HF Backbone: allenai/OLMo-1B-0724-hf...\n", + "[transformers] `torch_dtype` is deprecated! Use `dtype` instead!\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e74e598b5f0e4ad29d3a3356b80fe75c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading weights: 0%| | 0/114 [00:00 768\n", + "2026-10-11 23:26:25,227 - src.encoders.dna - INFO - DNA Encoder loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", + "2026-10-11 23:26:25,266 - src.modules.projector - INFO - Modality embedding: scale=0.0055, expected_norm=0.2489\n", + "2026-10-11 23:26:25,266 - src.modules.projector - INFO - ModalityProjector initialized: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm\n", + "2026-10-11 23:26:25,267 - src.model - INFO - Freezing Modality Encoders...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + ": UnifiedTransformer (3,835.3M)\n", + " backbone: OlmoForCausalLM (1,279.8M)\n", + " model: OlmoModel (1,176.8M)\n", + " lm_head: Linear (103.0M)\n", + " encoders: ModuleDict (2,549.8M)\n", + " dna: DNAEncoder (2,549.8M)\n", + " projectors: ModuleDict (5.8M)\n", + " dna: ModalityProjector (5.8M)\n", + " decoders: ModuleDict (0.0M)\n", + " text: LMHeadDecoder (0.0M)\n", + " text_decoder: LMHeadDecoder (0.0M)\n" + ] + } + ], + "source": [ + "from src.model import UnifiedTransformer\n", + "\n", + "base_vocab = len(llm_tok)\n", + "model = UnifiedTransformer(mc) # CPU for now\n", + "tok = model.backbone_tokenizer\n", + "\n", + "print(H.module_tree(model, max_depth=2))" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "cell-047", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "vocab 50296 -> 50296; embedding rows 50304\n", + "tag ids: {'dna_reference': (50292, 50293), 'dna_variant': (50294, 50295)}\n", + " id=50292 tokenize -> [''] |row norm| 0.000\n", + " id=50293 tokenize -> [''] |row norm| 0.000\n", + " id=50294 tokenize -> [''] |row norm| 0.000\n", + " id=50295 tokenize -> [''] |row norm| 0.000\n", + "mean |row norm| of original vocab: 0.199\n", + "(tag rows can be zero: the merge replaces each tag pair with DNA embeddings, so they're never looked up)\n" + ] + } + ], + "source": [ + "emb = model.backbone.get_input_embeddings().weight\n", + "print(f\"vocab {base_vocab} -> {len(tok)}; embedding rows {emb.shape[0]}\")\n", + "print(\"tag ids:\", mc.modality_start_end_token_indices)\n", + "for t in TAGS:\n", + " i = tok.convert_tokens_to_ids(t)\n", + " print(f\" {t:16s} id={i} tokenize -> {tok.tokenize(t)} |row norm| {emb[i].norm():.3f}\")\n", + "print(f\"mean |row norm| of original vocab: {emb[:50280].norm(dim=-1).mean():.3f}\")\n", + "print(\"(tag rows can be zero: the merge replaces each tag pair with DNA embeddings, so they're never looked up)\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "cell-048", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
paramstrainabletrainable %
component
LLM embeddings / lm_head20604518400.00
LLM backbone107374182400.00
DNA encoder (NT)254780122600.00
DNA encoder output Linear (2560->d_dna)196684800.00
DNA projector (MLP)57733135773313100.00
TOTAL383532839557733130.15
\n", + "
" + ], + "text/plain": [ + " params trainable trainable %\n", + "component \n", + "LLM embeddings / lm_head 206045184 0 0.00\n", + "LLM backbone 1073741824 0 0.00\n", + "DNA encoder (NT) 2547801226 0 0.00\n", + "DNA encoder output Linear (2560->d_dna) 1966848 0 0.00\n", + "DNA projector (MLP) 5773313 5773313 100.00\n", + "TOTAL 3835328395 5773313 0.15" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "P_BASE = H.param_table(model)\n", + "P_BASE" + ] + }, + { + "cell_type": "markdown", + "id": "cell-049", + "metadata": {}, + "source": [ + "### 9b. Modify the projector and add LoRA\n", + "\n", + "There's no `projector_type` switch. The DNA projector is always a `ModalityProjector` MLP (fc → GELU → fc → L2 token-norm → + modality embedding), and its **capacity** is set by two knobs: `projector_hidden_mult` (width) and `projector_num_layers` (depth). They're the `W2X`/`D2X` variants in `src/modules/projector.py:VARIANT_MAP`.\n", + "\n", + "LoRA is applied with torchtune's `apply_lora_torchtune(backbone, rank, alpha)`. That's what the trainer does when `training.lora_enabled=true`.\n", + "> `training.lora_target_modules` is **metadata only**: *every* `nn.Linear` in the backbone except `lm_head` gets an adapter." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "cell-050", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-10-11 23:26:25,548 - src.model - INFO - Initializing HF Backbone: allenai/OLMo-1B-0724-hf...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "cb5554853f1f4a4aad5b3c0cd5756e9c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading weights: 0%| | 0/114 [00:00 768\n", + "2026-10-11 23:26:30,288 - src.encoders.dna - INFO - DNA Encoder loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", + "2026-10-11 23:26:30,490 - src.modules.projector - INFO - Modality embedding: scale=0.0055, expected_norm=0.2489\n", + "2026-10-11 23:26:30,491 - src.modules.projector - INFO - ModalityProjector initialized: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm\n", + "2026-10-11 23:26:30,491 - src.model - INFO - Freezing Modality Encoders...\n", + "2026-10-11 23:26:36,815 - src.utils.lora_utils - INFO - torchtune LoRA: replaced 112 linear layers | trainable 12,058,624 / 1,291,845,632 params (0.933%)\n" + ] + }, + { + "data": { + "text/html": [ + "
shipped: hm=1, nl=2, no LoRA\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
 paramstrainabletrainable %
component   
LLM embeddings / lm_head206,045,18400.000000
LLM backbone1,073,741,82400.000000
DNA encoder (NT)2,547,801,22600.000000
DNA encoder output Linear (2560->d_dna)1,966,84800.000000
DNA projector (MLP)5,773,3135,773,313100.000000
TOTAL3,835,328,3955,773,3130.150000
\n", + "
modified: hm=2, nl=3, LoRA r=16\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
 paramstrainabletrainable %
component   
LLM embeddings / lm_head206,045,18400.000000
LLM backbone1,073,741,82400.000000
LoRA adapters12,058,62412,058,624100.000000
DNA encoder (NT)2,547,801,22600.000000
DNA encoder output Linear (2560->d_dna)1,966,84800.000000
DNA projector (MLP)28,323,84128,323,841100.000000
TOTAL3,869,937,54740,382,4651.040000
\n", + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "projector before: ModalityProjector(\n", + " (fc1): Linear(in_features=768, out_features=2048, bias=True)\n", + " (act): GELU(approximate='none')\n", + " (fc2): Linear(in_features=2048, out_features=2048, bias=True)\n", + ")\n", + "projector after: ModalityProjector(\n", + " (layers): ModuleList(\n", + " (0): Linear(in_features=768, out_features=4096, bias=True)\n", + " (1): GELU(approximate='none')\n", + " (2): Linear(in_features=4096, out_features=4096, bias=True)\n", + " (3): GELU(approximate='none')\n", + " (4): Linear(in_features=4096, out_features=2048, bias=True)\n", + " )\n", + ")\n" + ] + } + ], + "source": [ + "from src.utils.lora_utils import apply_lora_torchtune\n", + "MOD = [\"model.projector_hidden_mult=2\", \"model.projector_num_layers=3\"]\n", + "LORA = dict(rank=16, alpha=32.0)\n", + "\n", + "cfg_mod = compose_cfg(*BASE, \"training=bioreason_projector\", *MOD)\n", + "mc_mod = T.build_model_config(cfg_mod)\n", + "model_mod = UnifiedTransformer(mc_mod)\n", + "apply_lora_torchtune(model_mod.backbone, **LORA)\n", + "\n", + "H.side_by_side(P_BASE, H.param_table(model_mod),\n", + " titles=[\"shipped: hm=1, nl=2, no LoRA\", f\"modified: hm=2, nl=3, LoRA r={LORA['rank']}\"])\n", + "print(\"projector before:\", model.projectors[\"dna\"])\n", + "print(\"projector after: \", model_mod.projectors[\"dna\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "cell-051", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
shippedmodified
key
projector_hidden_mult12
projector_num_layers23
training.lora_enabled / r / alphafalsetrue / 16 / 32
\n", + "
" + ], + "text/plain": [ + " shipped modified\n", + "key \n", + "projector_hidden_mult 1 2\n", + "projector_num_layers 2 3\n", + "training.lora_enabled / r / alpha false true / 16 / 32" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Config diff (what you would put on the train.py command line)\n", + "from omegaconf import OmegaConf\n", + "a, b = OmegaConf.to_container(cfg.model), OmegaConf.to_container(cfg_mod.model)\n", + "display(pd.DataFrame([(k, a.get(k), b.get(k)) for k in sorted(set(a) | set(b)) if a.get(k) != b.get(k)]\n", + " + [(\"training.lora_enabled / r / alpha\", \"false\", f\"true / {LORA['rank']} / {LORA['alpha']:g}\")],\n", + " columns=[\"key\", \"shipped\", \"modified\"]).set_index(\"key\"))\n", + "del model_mod" + ] + }, + { + "cell_type": "markdown", + "id": "cell-052", + "metadata": {}, + "source": [ + "To train with these changes, add the same overrides to any stage: `model.projector_hidden_mult=2 model.projector_num_layers=3 training.lora_r=16 training.lora_alpha=32`. Note that a stage-1 checkpoint only loads into an SFT model whose projector has the **same shape**." + ] + }, + { + "cell_type": "markdown", + "id": "cell-053", + "metadata": {}, + "source": [ + "## 10. One batch end to end\n", + "\n", + "Build the **SFT** dataset with the builders `train.py` uses, take one example, and look at exactly what the model sees." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "cell-054", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO] DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "[INFO] DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "2026-10-11 23:26:37,120 - src.data.multimodal - INFO - DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "[INFO] dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", + "[INFO] dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", + "2026-10-11 23:26:37,123 - src.data.multimodal - INFO - dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", + "[INFO] dataset_overrides applied: dna_bioreason_variant_effect_coding <- {'skip': True}\n", + "[INFO] dataset_overrides applied: dna_bioreason_variant_effect_coding <- {'skip': True}\n", + "2026-10-11 23:26:37,124 - src.data.multimodal - INFO - dataset_overrides applied: dna_bioreason_variant_effect_coding <- {'skip': True}\n", + "[INFO] dataset_overrides applied: dna_bioreason_variant_effect_non_snv <- {'skip': True}\n", + "[INFO] dataset_overrides applied: dna_bioreason_variant_effect_non_snv <- {'skip': True}\n", + "2026-10-11 23:26:37,124 - src.data.multimodal - INFO - dataset_overrides applied: dna_bioreason_variant_effect_non_snv <- {'skip': True}\n", + "[INFO] dataset_overrides applied: ts_qa <- {'skip': True}\n", + "[INFO] dataset_overrides applied: ts_qa <- {'skip': True}\n", + "2026-10-11 23:26:37,125 - src.data.multimodal - INFO - dataset_overrides applied: ts_qa <- {'skip': True}\n", + "2026-10-11 23:26:37,225 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/google/tapas-base/resolve/main/tokenizer_config.json \"HTTP/1.1 307 Temporary Redirect\"\n", + "2026-10-11 23:26:37,236 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/google/tapas-base/00456266840bb0a319cd6748ebf7da3caf98816b/tokenizer_config.json \"HTTP/1.1 200 OK\"\n", + "2026-10-11 23:26:37,334 - httpx - INFO - HTTP Request: GET https://huggingface.co/api/models/google/tapas-base/tree/main/additional_chat_templates?recursive=false&expand=false \"HTTP/1.1 404 Not Found\"\n", + "2026-10-11 23:26:37,388 - httpx - INFO - HTTP Request: GET https://huggingface.co/api/models/google/tapas-base/tree/main?recursive=true&expand=false \"HTTP/1.1 200 OK\"\n", + "[INFO] Initializing Streaming Datasets for zone_a...\n", + "[INFO] Initializing Streaming Datasets for zone_a...\n", + "2026-10-11 23:26:37,476 - src.data.multimodal - INFO - Initializing Streaming Datasets for zone_a...\n", + "[WARNING] Skipping PixMo-Cap (Configured skip)\n", + "[WARNING] Skipping PixMo-Cap (Configured skip)\n", + "2026-10-11 23:26:37,477 - src.data.multimodal - WARNING - Skipping PixMo-Cap (Configured skip)\n", + "[WARNING] Skipping Molmo2-Cap (Configured skip)\n", + "[WARNING] Skipping Molmo2-Cap (Configured skip)\n", + "2026-10-11 23:26:37,478 - src.data.multimodal - WARNING - Skipping Molmo2-Cap (Configured skip)\n", + "[WARNING] Skipping PixMo-Points (Configured skip)\n", + "[WARNING] Skipping PixMo-Points (Configured skip)\n", + "2026-10-11 23:26:37,479 - src.data.multimodal - WARNING - Skipping PixMo-Points (Configured skip)\n", + "[WARNING] Skipping PixMo-Count (Configured skip)\n", + "[WARNING] Skipping PixMo-Count (Configured skip)\n", + "2026-10-11 23:26:37,479 - src.data.multimodal - WARNING - Skipping PixMo-Count (Configured skip)\n", + "[WARNING] Skipping Molmo2-VideoPoint (Configured skip)\n", + "[WARNING] Skipping Molmo2-VideoPoint (Configured skip)\n", + "2026-10-11 23:26:37,480 - src.data.multimodal - WARNING - Skipping Molmo2-VideoPoint (Configured skip)\n", + "[WARNING] Skipping The Well (Turbulence) (Configured skip)\n", + "[WARNING] Skipping The Well (Turbulence) (Configured skip)\n", + "2026-10-11 23:26:37,481 - src.data.multimodal - WARNING - Skipping The Well (Turbulence) (Configured skip)\n", + "[WARNING] Skipping The Well (MHD 64^3) (Configured skip)\n", + "[WARNING] Skipping The Well (MHD 64^3) (Configured skip)\n", + "2026-10-11 23:26:37,481 - src.data.multimodal - WARNING - Skipping The Well (MHD 64^3) (Configured skip)\n", + "[WARNING] Skipping The Well (Supernova) (Configured skip)\n", + "[WARNING] Skipping The Well (Supernova) (Configured skip)\n", + "2026-10-11 23:26:37,482 - src.data.multimodal - WARNING - Skipping The Well (Supernova) (Configured skip)\n", + "[WARNING] Skipping Berea Sandstone (Real) (Configured skip)\n", + "[WARNING] Skipping Berea Sandstone (Real) (Configured skip)\n", + "2026-10-11 23:26:37,482 - src.data.multimodal - WARNING - Skipping Berea Sandstone (Real) (Configured skip)\n", + "[WARNING] Skipping PDEBench (Problem Statements) (Configured skip)\n", + "[WARNING] Skipping PDEBench (Problem Statements) (Configured skip)\n", + "2026-10-11 23:26:37,483 - src.data.multimodal - WARNING - Skipping PDEBench (Problem Statements) (Configured skip)\n", + "[WARNING] Skipping Tulu 3 SFT (Configured skip)\n", + "[WARNING] Skipping Tulu 3 SFT (Configured skip)\n", + "2026-10-11 23:26:37,483 - src.data.multimodal - WARNING - Skipping Tulu 3 SFT (Configured skip)\n", + "[WARNING] Skipping ChatTS Align (Synthetic) (Configured skip)\n", + "[WARNING] Skipping ChatTS Align (Synthetic) (Configured skip)\n", + "2026-10-11 23:26:37,484 - src.data.multimodal - WARNING - Skipping ChatTS Align (Synthetic) (Configured skip)\n", + "[WARNING] Skipping TS Reasoning (Proxy) (Configured skip)\n", + "[WARNING] Skipping TS Reasoning (Proxy) (Configured skip)\n", + "2026-10-11 23:26:37,485 - src.data.multimodal - WARNING - Skipping TS Reasoning (Proxy) (Configured skip)\n", + "[WARNING] Skipping ChatTime (Verified Available)\n", + "[WARNING] Skipping ChatTime (Verified Available)\n", + "2026-10-11 23:26:37,485 - src.data.multimodal - WARNING - Skipping ChatTime (Verified Available)\n", + "[WARNING] Skipping Time-MMD (Economy) (Configured skip)\n", + "[WARNING] Skipping Time-MMD (Economy) (Configured skip)\n", + "2026-10-11 23:26:37,486 - src.data.multimodal - WARNING - Skipping Time-MMD (Economy) (Configured skip)\n", + "[WARNING] Skipping TableLLM (Reasoning) (Configured skip)\n", + "[WARNING] Skipping TableLLM (Reasoning) (Configured skip)\n", + "2026-10-11 23:26:37,486 - src.data.multimodal - WARNING - Skipping TableLLM (Reasoning) (Configured skip)\n", + "[WARNING] Skipping TableInstruct (Gen) (Configured skip)\n", + "[WARNING] Skipping TableInstruct (Gen) (Configured skip)\n", + "2026-10-11 23:26:37,487 - src.data.multimodal - WARNING - Skipping TableInstruct (Gen) (Configured skip)\n", + "[WARNING] Skipping UniTabE (Structure) (Configured skip)\n", + "[WARNING] Skipping UniTabE (Structure) (Configured skip)\n", + "2026-10-11 23:26:37,488 - src.data.multimodal - WARNING - Skipping UniTabE (Structure) (Configured skip)\n", + "[WARNING] Skipping ChEBI-20 (Global) (Configured skip)\n", + "[WARNING] Skipping ChEBI-20 (Global) (Configured skip)\n", + "2026-10-11 23:26:37,488 - src.data.multimodal - WARNING - Skipping ChEBI-20 (Global) (Configured skip)\n", + "[WARNING] Skipping ChEBI-20-MM (Grounding) (Configured skip)\n", + "[WARNING] Skipping ChEBI-20-MM (Grounding) (Configured skip)\n", + "2026-10-11 23:26:37,489 - src.data.multimodal - WARNING - Skipping ChEBI-20-MM (Grounding) (Configured skip)\n", + "[WARNING] Skipping GraphInstruct (Configured skip)\n", + "[WARNING] Skipping GraphInstruct (Configured skip)\n", + "2026-10-11 23:26:37,489 - src.data.multimodal - WARNING - Skipping GraphInstruct (Configured skip)\n", + "[WARNING] Skipping Matbench (Crystal) (Configured skip)\n", + "[WARNING] Skipping Matbench (Crystal) (Configured skip)\n", + "2026-10-11 23:26:37,490 - src.data.multimodal - WARNING - Skipping Matbench (Crystal) (Configured skip)\n", + "[WARNING] Skipping Verilog (Circuit) (Configured skip)\n", + "[WARNING] Skipping Verilog (Circuit) (Configured skip)\n", + "2026-10-11 23:26:37,490 - src.data.multimodal - WARNING - Skipping Verilog (Circuit) (Configured skip)\n", + "[INFO] [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", + "[INFO] [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", + "2026-10-11 23:26:37,491 - src.data.multimodal - INFO - [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", + "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n", + "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n", + "2026-10-11 23:26:37,497 - src.data.multimodal - INFO - [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DEBUG: Loading 1 Arrow files directly...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[WARNING] Skipping Variant Effect (Coding) (Configured skip)\n", + "[WARNING] Skipping Variant Effect (Coding) (Configured skip)\n", + "2026-10-11 23:26:37,627 - src.data.multimodal - WARNING - Skipping Variant Effect (Coding) (Configured skip)\n", + "[WARNING] Skipping Variant Effect (Non-SNV) (Configured skip)\n", + "[WARNING] Skipping Variant Effect (Non-SNV) (Configured skip)\n", + "2026-10-11 23:26:37,627 - src.data.multimodal - WARNING - Skipping Variant Effect (Non-SNV) (Configured skip)\n", + "[INFO] [Init] Pre-Calculated Active Modalities: []\n", + "[INFO] [Init] Pre-Calculated Active Modalities: []\n", + "2026-10-11 23:26:37,628 - src.data.multimodal - INFO - [Init] Pre-Calculated Active Modalities: []\n", + "2026-10-11 23:26:37,629 - src.train - INFO - Using MultimodalCollator with max_seq_length=None (is_interleaved_qa=True, passthrough_time_series=False)\n", + "[INFO] [streaming manifest] dna_bioreason: expected ~1,159 samples (train split) samples/epoch\n", + "[INFO] [streaming manifest] dna_bioreason: expected ~1,159 samples (train split) samples/epoch\n", + "2026-10-11 23:26:37,630 - src.data.multimodal - INFO - [streaming manifest] dna_bioreason: expected ~1,159 samples (train split) samples/epoch\n", + "[INFO] Active Modalities for Sampling: ['dna']\n", + "[INFO] Active Modalities for Sampling: ['dna']\n", + "2026-10-11 23:26:37,630 - src.data.multimodal - INFO - Active Modalities for Sampling: ['dna']\n", + "[INFO] Modality dna: ['dna_bioreason'] (Weights: [1.0])\n", + "[INFO] Modality dna: ['dna_bioreason'] (Weights: [1.0])\n", + "2026-10-11 23:26:37,631 - src.data.multimodal - INFO - Modality dna: ['dna_bioreason'] (Weights: [1.0])\n", + "[transformers] Ignoring clean_up_tokenization_spaces=True for BPE tokenizer GPTNeoXTokenizer. The clean_up_tokenization post-processing step is designed for WordPiece tokenizers and is destructive for BPE (it strips spaces before punctuation). Set clean_up_tokenization_spaces=False to suppress this warning, or set clean_up_tokenization_spaces_for_bpe_even_though_it_will_corrupt_output=True to force cleanup anyway.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_metadata 'P T' = '214 524': 214 prompt tokens (no loss) + 524 target tokens (loss)\n", + "DNA: ref (1024,), var (1024,)\n", + "merged length = 738 text - 4 tags + 2 x 1024 DNA slots = 2782\n" + ] + }, + { + "data": { + "text/html": [ + "
grey = prompt (label -100, no loss)   green = target (loss)   blue = DNA splice tag
<|user|>\n", + "Reference sequence: <dna_ref_start> [1024 DNA embeddings]<dna_ref_end>\n", + "Variant sequence: <dna_var_start> [1024 DNA embeddings]<dna_var_end>\n", + "Chromosome Number: 4\n", + "Network Definition of the pathway: KIT* -> PI3K -> PIP3 -> AKT -| BAD\n", + "Genes in the pathway: KIT; KIT proto-oncogene receptor tyrosine kinase | PIK3CA; phosphatidylinositol-4,5-bisphosphate 3-kinase catalytic subunit alpha | PIK3CB; phosphatidylinositol-4,5-bisphosphate 3-kinase catalytic subunit beta | PIK3CD; phosphatidylinositol-4,5-bisphosphate 3-kinase catalytic subunit delta | AKT1; AKT serine/threonine kinase 1 | AKT2; AKT serine/threonine kinase 2 | AKT3; AKT serine/threonine kinase 3 | BAD; BCL2 associated agonist of cell death\n", + "\n", + "Given this context, what is the biological effect of this KIT allele, specifically what disease does this contribute to?\n", + "<|assistant|>\n", + "<think>\n", + "Step 1: The variant is an A>T substitution at position 54733154 on chromosome 4, occurring in the KIT gene, which encodes a receptor tyrosine kinase (RTK).\n", + "Step 2: KIT is a transmembrane receptor that, when bound by its ligand stem cell factor (SCF), activates multiple downstream signaling pathways including PI3K/AKT.\n", + "Step 3: This mutation likely occurs in a critical functional domain of KIT, resulting in constitutive activation of the receptor even in the absence of its ligand.\n", + "Step 4: Constitutively active KIT leads to persistent activation of the PI3K enzyme complex, which consists of catalytic subunits (PIK3CA, PIK3CB, or PIK3CD) and regulatory subunits.\n", + "Step 5: Activated PI3K phosphorylates phosphatidylinositol-4,5-bisphosphate (PIP2) to generate phosphatidylinositol-3,4,5-trisphosphate (PIP3), a critical second messenger.\n", + "Step 6: PIP3 recruits AKT (AKT1, AKT2, or AKT3) to the plasma membrane where it becomes activated through phosphorylation.\n", + "Step 7: Activated AKT phosphorylates numerous downstream targets, including the pro-apoptotic protein BAD.\n", + "Step 8: Phosphorylation of BAD by AKT causes it to dissociate from anti-apoptotic BCL-2 family proteins and become sequestered by 14-3-3 proteins, preventing its pro-apoptotic function.\n", + "Step 9: Inhibition of BAD promotes cell survival by preventing apoptosis, while other AKT targets promote cell proliferation and metabolism.\n", + "Step 10: The hyperactivation of this KIT→PI3K→AKT pathway due to the KIT mutation leads to increased survival and proliferation of hematopoietic cells.\n", + "Step 11: This uncontrolled proliferation and enhanced survival of myeloid progenitor cells contributes to the development of Acute Myeloid Leukemia (AML).\n", + "Step 12: In the context of AML pathogenesis, this KIT mutation represents the 'first event' in leukemic transformation, characterized by disordered cell growth and upregulation of cell survival genes.\n", + "This KIT mutation likely results in constitutive activation of the receptor tyrosine kinase, leading to hyperactivation of the PI3K/AKT signaling pathway and inhibition of the pro-apoptotic BAD protein. This promotes increased cell survival and proliferation, contributing to the development of Acute Myeloid Leukemia (AML) through disordered cell growth and upregulation of survival genes.\n", + "</think>\n", + "\n", + "Answer: <answer>acute myeloid leukemia</answer><|endoftext|>
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cfg_sft = compose_cfg(*BASE, \"training=bioreason_sft\", \"training.resume_weights_only=null\")\n", + "tc_sft = T.build_training_config(cfg_sft, mc, output_dir=\"/tmp\", seed=0)\n", + "ds_sft = T.build_streaming_dataset(cfg_sft, tok, mc, tc_sft,\n", + " T.build_bio_dataset_kwargs(\"bioreason_sft\", tc_sft, mc),\n", + " max_seq_length=2048)\n", + "collate = T.build_collator(tok, mc, use_bucketed_collator=False, collator_max_seq=2048)\n", + "ex = next(iter(ds_sft))\n", + "P, Tn = map(int, ex[\"_metadata\"].split())\n", + "special = {i: t for t in TAGS for i in [tok.convert_tokens_to_ids(t)]}\n", + "n_spans = sum(1 for t in ex[\"text\"].tolist() if t in special) // 2\n", + "assert n_spans == 2, \"no DNA tag tokens in the prompt - DNA would be silently dropped (see section 8)\"\n", + "\n", + "print(f\"_metadata 'P T' = '{ex['_metadata']}': {P} prompt tokens (no loss) + {Tn} target tokens (loss)\")\n", + "print(f\"DNA: ref {tuple(ex['dna']['dna_reference']['input_ids'].shape)}, var {tuple(ex['dna']['dna_variant']['input_ids'].shape)}\")\n", + "print(f\"merged length = {len(ex['text'])} text - {2 * n_spans} tags + {n_spans} x {mc.max_dna_length} DNA slots \"\n", + " f\"= {len(ex['text']) - 2 * n_spans + n_spans * mc.max_dna_length}\")\n", + "display(HTML(H.token_view(tok, ex[\"text\"].tolist(), P, special, mc.max_dna_length)))" + ] + }, + { + "cell_type": "markdown", + "id": "cell-055", + "metadata": {}, + "source": [ + "The **projector** stage formats the same row differently. Its `_metadata` is the sentinel `[dna_bioreason]`, which means a causal-LM loss over the whole sequence. That gives the freshly initialized projector a dense alignment signal before SFT narrows the loss to the answer.\n", + "\n", + "That merged length (~2.8k) is over the 2048 default of `MAX_SEQ_LENGTH`, so the merged-length guard (`model.py`, issue #123) would raise. That's why section 1 sets `MAX_SEQ_LENGTH=4096` and every job inherits it.\n", + "\n", + "> **Efficiency tip from section 4:** KEGG and variant-effect references are ~4 kb, about **672–687 NT tokens**, yet every span is padded to 1024. `training.max_dna_length=768` would cut ~25% of the sequence with no truncation. It's not applied here, so the demo matches the shipped config.\n", + "\n", + "Now run the forward pass on a tile, the way the trainer calls it (`logits, loss = model(batch, labels=batch[\"text\"])`):" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "cell-056", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-10-11 23:26:43,890 - src.model - INFO - [MODEL] Forwarding Backbone: OlmoForCausalLM\n", + "2026-10-11 23:26:44,807 - src.model - INFO - [MODEL] Computed Loss: 8.93231201171875\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "logits (1, 2782, 50304) (= merged length, DNA included) loss 8.932\n", + "The loss is high because the projector is untrained: 2,048 out-of-distribution vectors in the context. Stage 1 fixes that.\n" + ] + } + ], + "source": [ + "def to_dev(x):\n", + " if torch.is_tensor(x): return x.to(dev)\n", + " if isinstance(x, dict): return {k: to_dev(v) for k, v in x.items()}\n", + " return x\n", + "\n", + "model.to(dev).eval()\n", + "batch = to_dev(collate([ex]))\n", + "with torch.no_grad(), torch.autocast(\"xpu\", dtype=torch.bfloat16):\n", + " logits, loss = model(batch, labels=batch[\"text\"])\n", + "print(f\"logits {tuple(logits.shape)} (= merged length, DNA included) loss {loss.item():.3f}\")\n", + "print(\"The loss is high because the projector is untrained: 2,048 out-of-distribution vectors in the context. Stage 1 fixes that.\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-057", + "metadata": {}, + "source": [ + "## 11. Local smoke run on this node\n", + "\n", + "Before burning queue time, run **20 projector steps on all 12 tiles** of this interactive node, using the same `scripts/training/submit_bioreason_projector.sh` the batch job uses. The scripts already send checkpoints to `$HYDRA_RUN_DIR` on `/flare`. The notebook runs a copy made by `H.patch_stage_script`, which drops the projector script's stray `source .venv/bin/activate` (a CUDA torch venv layered over the XPU one). For older script versions, it also adds the missing `hydra.run.dir=`.\n", + "\n", + "The kernel is holding tile 0, so free it first." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "cell-058", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "68d67\n", + "< source \"${PROJ_DIR}/.venv/bin/activate\"\n" + ] + } + ], + "source": [ + "del model, logits, loss, batch\n", + "import gc; gc.collect(); torch.xpu.empty_cache()\n", + "\n", + "SCRIPTS = Path(PRISM_OUTPUT_ROOT, \"demo_scripts\", RUN_TAG)\n", + "PROJ_SH = H.patch_stage_script(REPO / \"applications/bioreason/scripts/training/submit_bioreason_projector.sh\", SCRIPTS / \"projector.sh\")\n", + "SFT_SH = H.patch_stage_script(REPO / \"applications/bioreason/scripts/training/submit_bioreason_sft.sh\", SCRIPTS / \"sft.sh\")\n", + "GRPO_SH = REPO / \"applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh\" # native HYDRA_RUN_DIR support\n", + "EVAL_SH = REPO / \"applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh\"\n", + "!diff {REPO}/applications/bioreason/scripts/training/submit_bioreason_projector.sh {PROJ_SH}" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "cell-059", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Step 0: Loss 9.5068\n", + "Training Zone A: 50%|█████ | 10/20 [00:08<00:05, 1.87it/s, loss=9.42]Step 10: Loss 9.4199\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[2026-10-11 23:28:20,589][accelerate.accelerator][INFO] - [RANK 0] Saving current state to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final\n" + ] + } + ], + "source": [ + "SMOKE_DIR = f\"{PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/{RUN_TAG}\"\n", + "SMOKE_LOG = f\"{PRISM_OUTPUT_ROOT}/demo_logs/{RUN_TAG}/smoke-projector.log\"\n", + "Path(SMOKE_LOG).parent.mkdir(parents=True, exist_ok=True)\n", + "smoke_env = \" \".join(f\"{k}={v}\" for k, v in {\n", + " \"RUN_ID\": \"DEMO-SMOKE-PROJECTOR\", \"HYDRA_RUN_DIR\": SMOKE_DIR,\n", + " \"MAX_STEPS\": 20, \"WARMUP_STEPS\": 2, \"SAVE_EVERY\": 20}.items())\n", + "# ~5-8 min: model load on 12 ranks + 20 steps. Progress lines only; full log in SMOKE_LOG.\n", + "!env {smoke_env} bash {PROJ_SH} > {SMOKE_LOG} 2>&1; grep -E \"Step [0-9]+: Loss|Saving|Error|error:\" {SMOKE_LOG} | tail -25" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "cell-060", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "model.safetensors random_states_1.pkl random_states_6.pkl\n", + "optimizer.bin\t random_states_2.pkl random_states_7.pkl\n", + "random_states_0.pkl random_states_3.pkl random_states_8.pkl\n", + "random_states_10.pkl random_states_4.pkl random_states_9.pkl\n", + "random_states_11.pkl random_states_5.pkl training_state.json\n" + ] + } + ], + "source": [ + "H.plot_logs({\"smoke projector\": SMOKE_LOG}); plt.show()\n", + "!ls {SMOKE_DIR}/checkpoints/step_final" + ] + }, + { + "cell_type": "markdown", + "id": "cell-061", + "metadata": {}, + "source": [ + "## 12. Submitting all four stages with `qsub`\n", + "\n", + "Each stage's output path is **decided here**, from `RUN_TAG`, so the next stage can be told where its input will be *before* it exists. PBS dependencies (`-W depend=afterok:`) hold each job until the previous one succeeds.\n", + "\n", + "```\n", + "projector (debug, 1h) ──afterok──► SFT (prod) ──afterok──► GRPO (prod, 1 tile) ──afterok──► eval SFT+GRPO\n", + " └──afterok──► eval SFT\n", + "```\n", + "\n", + "Step counts here are **demo-sized** (a few hundred steps); the YAML defaults are 5k / 10k / 1k. Set `SUBMIT = True` to actually submit. Otherwise the cell prints the `qsub` commands.\n", + "\n", + "> If `qsub` isn't available on this compute node, copy the printed commands and run them from a login node, from `REPO`." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "cell-062", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-PROJECTOR-KEGG/demo-20261011-2324,MAX_STEPS=100,SAVE_EVERY=50 -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/projector.log /lus/flare/projects/ModCon/abalaji/output/demo_scripts/demo-20261011-2324/projector.sh\n", + "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-SFT-KEGG/demo-20261011-2324,STAGE1_CKPT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-PROJECTOR-KEGG/demo-20261011-2324/checkpoints/step_final,MAX_STEPS=500 -W depend=afterok:DRYRUN -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/sft.log -l walltime=02:00:00 /lus/flare/projects/ModCon/abalaji/output/demo_scripts/demo-20261011-2324/sft.sh\n", + "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-GRPO-KEGG/demo-20261011-2324,STAGE1_CKPT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,MAX_STEPS=100,SAVE_EVERY=50 -W depend=afterok:DRYRUN -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/grpo.log /home/abalaji/projects/modcon/release/BaseMM_PRISM/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh\n", + "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,CKPT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,OUTPUT=/lus/flare/projects/ModCon/abalaji/output/BIOREASON-EVAL-KEGG/demo-20261011-2324/sft.json -W depend=afterok:DRYRUN -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/eval-sft.log /home/abalaji/projects/modcon/release/BaseMM_PRISM/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh\n", + "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,CKPT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,GRPO_ADAPTER=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-GRPO-KEGG/demo-20261011-2324/checkpoints/step_final/lora_adapter.pt,OUTPUT=/lus/flare/projects/ModCon/abalaji/output/BIOREASON-EVAL-KEGG/demo-20261011-2324/sft+grpo.json -W depend=afterok:DRYRUN -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/eval-sft+grpo.log /home/abalaji/projects/modcon/release/BaseMM_PRISM/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh\n" + ] + }, + { + "data": { + "text/plain": [ + "{'projector': 'DRYRUN',\n", + " 'sft': 'DRYRUN',\n", + " 'grpo': 'DRYRUN',\n", + " 'eval-sft': 'DRYRUN',\n", + " 'eval-sft+grpo': 'DRYRUN'}" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "SUBMIT = False # <- flip to submit for real\n", + "\n", + "RUN = {s: f\"{PRISM_CKPT_ROOT}/BIOREASON-{s.upper()}-KEGG/{RUN_TAG}\" for s in (\"projector\", \"sft\", \"grpo\")}\n", + "CKPT_PROJ = f\"{RUN['projector']}/checkpoints/step_final\"\n", + "CKPT_SFT = f\"{RUN['sft']}/checkpoints/step_final\"\n", + "GRPO_ADPT = f\"{RUN['grpo']}/checkpoints/step_final/lora_adapter.pt\"\n", + "EVAL_OUT = {k: f\"{PRISM_OUTPUT_ROOT}/BIOREASON-EVAL-KEGG/{RUN_TAG}/{k}.json\" for k in (\"sft\", \"sft+grpo\")}\n", + "LOGS = {k: f\"{PRISM_OUTPUT_ROOT}/demo_logs/{RUN_TAG}/{k}.log\"\n", + " for k in (\"projector\", \"sft\", \"grpo\", \"eval-sft\", \"eval-sft+grpo\")}\n", + "common = {k: ENV[k] for k in (\"PRISM_DATA_ROOT\", \"PRISM_OUTPUT_ROOT\", \"PRISM_CKPT_ROOT\", \"PRISM_VENV\", \"WANDB_MODE\", \"MAX_SEQ_LENGTH\")} | {\"RUN_TAG\": RUN_TAG}\n", + "\n", + "JOBS = {}\n", + "q = lambda *a, **k: H.qsub(*a, cwd=str(REPO), dry_run=not SUBMIT, **k)\n", + "JOBS[\"projector\"] = q(PROJ_SH, {**common, \"HYDRA_RUN_DIR\": RUN[\"projector\"], \"MAX_STEPS\": 100, \"SAVE_EVERY\": 50},\n", + " log=LOGS[\"projector\"])\n", + "JOBS[\"sft\"] = q(SFT_SH, {**common, \"HYDRA_RUN_DIR\": RUN[\"sft\"], \"STAGE1_CKPT\": CKPT_PROJ, \"MAX_STEPS\": 500},\n", + " depend=JOBS[\"projector\"], log=LOGS[\"sft\"], walltime=\"02:00:00\")\n", + "JOBS[\"grpo\"] = q(GRPO_SH, {**common, \"HYDRA_RUN_DIR\": RUN[\"grpo\"], \"STAGE1_CKPT\": CKPT_SFT,\n", + " \"MAX_STEPS\": 100, \"SAVE_EVERY\": 50},\n", + " depend=JOBS[\"sft\"], log=LOGS[\"grpo\"])\n", + "JOBS[\"eval-sft\"] = q(EVAL_SH, {**common, \"CKPT\": CKPT_SFT, \"OUTPUT\": EVAL_OUT[\"sft\"]},\n", + " depend=JOBS[\"sft\"], log=LOGS[\"eval-sft\"])\n", + "JOBS[\"eval-sft+grpo\"] = q(EVAL_SH, {**common, \"CKPT\": CKPT_SFT, \"GRPO_ADAPTER\": GRPO_ADPT, \"OUTPUT\": EVAL_OUT[\"sft+grpo\"]},\n", + " depend=JOBS[\"grpo\"], log=LOGS[\"eval-sft+grpo\"])\n", + "JOBS" + ] + }, + { + "cell_type": "markdown", + "id": "cell-063", + "metadata": {}, + "source": [ + "## 13. Monitoring and results\n", + "\n", + "Press **Refresh** at any time. It shows `qstat` for the chain (including finished jobs), the loss (and GRPO reward) curves parsed from each log, and the tail of each log. Each run gets its own panel, so there are no shared or dual axes." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "cell-064", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a3566be993b64c92824963b746743a46", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Button(description='Refresh', icon='refresh', style=ButtonStyle()), Output()))" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H.monitor(list(JOBS.values()), {k: LOGS[k] for k in (\"projector\", \"sft\", \"grpo\")})" + ] + }, + { + "cell_type": "markdown", + "id": "cell-065", + "metadata": {}, + "source": [ + "### Results\n", + "\n", + "Once the eval jobs finish, compare SFT with SFT + GRPO on the KEGG test split (146 variants). `format_rate` is the share of completions with a well-formed `…`, which GRPO's format rewards push up." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "cell-066", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "no eval results yet\n" + ] + } + ], + "source": [ + "import json\n", + "res = {k: json.load(open(p)) for k, p in EVAL_OUT.items() if Path(p).exists()}\n", + "if not res:\n", + " print(\"no eval results yet\")\n", + "else:\n", + " display(pd.DataFrame({k: {\"accuracy\": r[\"accuracy\"], \"format_rate\": r[\"format_rate\"], \"n\": r[\"n\"]}\n", + " for k, r in res.items()}).T)\n", + " last = list(res)[-1]\n", + " per = pd.DataFrame(res[last][\"per_label\"]).T.assign(acc=lambda d: d.correct / d.n).sort_values(\"n\", ascending=False)\n", + " display(per.head(15))" + ] + }, + { + "cell_type": "markdown", + "id": "cell-067", + "metadata": {}, + "source": [ + "### Quick in-process check\n", + "\n", + "`eval_kegg_aurora.py` is importable, so you can score a checkpoint right here on one tile. It uses the same weight loading and prompt rendering as the batch eval." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "cell-068", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-10-11 23:28:34,630 - src.model - INFO - Initializing HF Backbone: allenai/OLMo-1B-0724-hf...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "checkpoint: /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "80b769076d3940b988e929fe893df6a8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading weights: 0%| | 0/114 [00:00 768\n", + "2026-10-11 23:28:38,927 - src.encoders.dna - INFO - DNA Encoder loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", + "2026-10-11 23:28:38,969 - src.modules.projector - INFO - Modality embedding: scale=0.0055, expected_norm=0.2489\n", + "2026-10-11 23:28:38,969 - src.modules.projector - INFO - ModalityProjector initialized: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm\n", + "2026-10-11 23:28:38,970 - src.model - INFO - Freezing Modality Encoders...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Resume] Loading model weights from /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final/model.safetensors\n", + "[Resume] Loaded 646 weight tensors successfully\n", + "[Resume] Checkpoint was saved at step final\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[INFO] DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "[INFO] DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "2026-10-11 23:28:53,483 - src.data.multimodal - INFO - DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "[INFO] dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", + "[INFO] dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", + "2026-10-11 23:28:53,485 - src.data.multimodal - INFO - dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", + "[INFO] dataset_overrides applied: dna_bioreason_variant_effect_coding <- {'skip': True}\n", + "[INFO] dataset_overrides applied: dna_bioreason_variant_effect_coding <- {'skip': True}\n", + "2026-10-11 23:28:53,486 - src.data.multimodal - INFO - dataset_overrides applied: dna_bioreason_variant_effect_coding <- {'skip': True}\n", + "[INFO] dataset_overrides applied: dna_bioreason_variant_effect_non_snv <- {'skip': True}\n", + "[INFO] dataset_overrides applied: dna_bioreason_variant_effect_non_snv <- {'skip': True}\n", + "2026-10-11 23:28:53,487 - src.data.multimodal - INFO - dataset_overrides applied: dna_bioreason_variant_effect_non_snv <- {'skip': True}\n", + "[INFO] dataset_overrides applied: ts_qa <- {'skip': True}\n", + "[INFO] dataset_overrides applied: ts_qa <- {'skip': True}\n", + "2026-10-11 23:28:53,487 - src.data.multimodal - INFO - dataset_overrides applied: ts_qa <- {'skip': True}\n", + "2026-10-11 23:28:53,545 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/google/tapas-base/resolve/main/tokenizer_config.json \"HTTP/1.1 307 Temporary Redirect\"\n", + "2026-10-11 23:28:53,551 - httpx - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/google/tapas-base/00456266840bb0a319cd6748ebf7da3caf98816b/tokenizer_config.json \"HTTP/1.1 200 OK\"\n", + "2026-10-11 23:28:53,600 - httpx - INFO - HTTP Request: GET https://huggingface.co/api/models/google/tapas-base/tree/main/additional_chat_templates?recursive=false&expand=false \"HTTP/1.1 404 Not Found\"\n", + "2026-10-11 23:28:53,658 - httpx - INFO - HTTP Request: GET https://huggingface.co/api/models/google/tapas-base/tree/main?recursive=true&expand=false \"HTTP/1.1 200 OK\"\n", + "[INFO] Initializing Streaming Datasets for zone_a...\n", + "[INFO] Initializing Streaming Datasets for zone_a...\n", + "2026-10-11 23:28:53,685 - src.data.multimodal - INFO - Initializing Streaming Datasets for zone_a...\n", + "[WARNING] Skipping PixMo-Cap (Configured skip)\n", + "[WARNING] Skipping PixMo-Cap (Configured skip)\n", + "2026-10-11 23:28:53,686 - src.data.multimodal - WARNING - Skipping PixMo-Cap (Configured skip)\n", + "[WARNING] Skipping Molmo2-Cap (Configured skip)\n", + "[WARNING] Skipping Molmo2-Cap (Configured skip)\n", + "2026-10-11 23:28:53,687 - src.data.multimodal - WARNING - Skipping Molmo2-Cap (Configured skip)\n", + "[WARNING] Skipping PixMo-Points (Configured skip)\n", + "[WARNING] Skipping PixMo-Points (Configured skip)\n", + "2026-10-11 23:28:53,688 - src.data.multimodal - WARNING - Skipping PixMo-Points (Configured skip)\n", + "[WARNING] Skipping PixMo-Count (Configured skip)\n", + "[WARNING] Skipping PixMo-Count (Configured skip)\n", + "2026-10-11 23:28:53,688 - src.data.multimodal - WARNING - Skipping PixMo-Count (Configured skip)\n", + "[WARNING] Skipping Molmo2-VideoPoint (Configured skip)\n", + "[WARNING] Skipping Molmo2-VideoPoint (Configured skip)\n", + "2026-10-11 23:28:53,689 - src.data.multimodal - WARNING - Skipping Molmo2-VideoPoint (Configured skip)\n", + "[WARNING] Skipping The Well (Turbulence) (Configured skip)\n", + "[WARNING] Skipping The Well (Turbulence) (Configured skip)\n", + "2026-10-11 23:28:53,690 - src.data.multimodal - WARNING - Skipping The Well (Turbulence) (Configured skip)\n", + "[WARNING] Skipping The Well (MHD 64^3) (Configured skip)\n", + "[WARNING] Skipping The Well (MHD 64^3) (Configured skip)\n", + "2026-10-11 23:28:53,690 - src.data.multimodal - WARNING - Skipping The Well (MHD 64^3) (Configured skip)\n", + "[WARNING] Skipping The Well (Supernova) (Configured skip)\n", + "[WARNING] Skipping The Well (Supernova) (Configured skip)\n", + "2026-10-11 23:28:53,691 - src.data.multimodal - WARNING - Skipping The Well (Supernova) (Configured skip)\n", + "[WARNING] Skipping Berea Sandstone (Real) (Configured skip)\n", + "[WARNING] Skipping Berea Sandstone (Real) (Configured skip)\n", + "2026-10-11 23:28:53,692 - src.data.multimodal - WARNING - Skipping Berea Sandstone (Real) (Configured skip)\n", + "[WARNING] Skipping PDEBench (Problem Statements) (Configured skip)\n", + "[WARNING] Skipping PDEBench (Problem Statements) (Configured skip)\n", + "2026-10-11 23:28:53,692 - src.data.multimodal - WARNING - Skipping PDEBench (Problem Statements) (Configured skip)\n", + "[WARNING] Skipping Tulu 3 SFT (Configured skip)\n", + "[WARNING] Skipping Tulu 3 SFT (Configured skip)\n", + "2026-10-11 23:28:53,693 - src.data.multimodal - WARNING - Skipping Tulu 3 SFT (Configured skip)\n", + "[WARNING] Skipping ChatTS Align (Synthetic) (Configured skip)\n", + "[WARNING] Skipping ChatTS Align (Synthetic) (Configured skip)\n", + "2026-10-11 23:28:53,693 - src.data.multimodal - WARNING - Skipping ChatTS Align (Synthetic) (Configured skip)\n", + "[WARNING] Skipping TS Reasoning (Proxy) (Configured skip)\n", + "[WARNING] Skipping TS Reasoning (Proxy) (Configured skip)\n", + "2026-10-11 23:28:53,694 - src.data.multimodal - WARNING - Skipping TS Reasoning (Proxy) (Configured skip)\n", + "[WARNING] Skipping ChatTime (Verified Available)\n", + "[WARNING] Skipping ChatTime (Verified Available)\n", + "2026-10-11 23:28:53,694 - src.data.multimodal - WARNING - Skipping ChatTime (Verified Available)\n", + "[WARNING] Skipping Time-MMD (Economy) (Configured skip)\n", + "[WARNING] Skipping Time-MMD (Economy) (Configured skip)\n", + "2026-10-11 23:28:53,695 - src.data.multimodal - WARNING - Skipping Time-MMD (Economy) (Configured skip)\n", + "[WARNING] Skipping TableLLM (Reasoning) (Configured skip)\n", + "[WARNING] Skipping TableLLM (Reasoning) (Configured skip)\n", + "2026-10-11 23:28:53,695 - src.data.multimodal - WARNING - Skipping TableLLM (Reasoning) (Configured skip)\n", + "[WARNING] Skipping TableInstruct (Gen) (Configured skip)\n", + "[WARNING] Skipping TableInstruct (Gen) (Configured skip)\n", + "2026-10-11 23:28:53,696 - src.data.multimodal - WARNING - Skipping TableInstruct (Gen) (Configured skip)\n", + "[WARNING] Skipping UniTabE (Structure) (Configured skip)\n", + "[WARNING] Skipping UniTabE (Structure) (Configured skip)\n", + "2026-10-11 23:28:53,697 - src.data.multimodal - WARNING - Skipping UniTabE (Structure) (Configured skip)\n", + "[WARNING] Skipping ChEBI-20 (Global) (Configured skip)\n", + "[WARNING] Skipping ChEBI-20 (Global) (Configured skip)\n", + "2026-10-11 23:28:53,698 - src.data.multimodal - WARNING - Skipping ChEBI-20 (Global) (Configured skip)\n", + "[WARNING] Skipping ChEBI-20-MM (Grounding) (Configured skip)\n", + "[WARNING] Skipping ChEBI-20-MM (Grounding) (Configured skip)\n", + "2026-10-11 23:28:53,698 - src.data.multimodal - WARNING - Skipping ChEBI-20-MM (Grounding) (Configured skip)\n", + "[WARNING] Skipping GraphInstruct (Configured skip)\n", + "[WARNING] Skipping GraphInstruct (Configured skip)\n", + "2026-10-11 23:28:53,699 - src.data.multimodal - WARNING - Skipping GraphInstruct (Configured skip)\n", + "[WARNING] Skipping Matbench (Crystal) (Configured skip)\n", + "[WARNING] Skipping Matbench (Crystal) (Configured skip)\n", + "2026-10-11 23:28:53,699 - src.data.multimodal - WARNING - Skipping Matbench (Crystal) (Configured skip)\n", + "[WARNING] Skipping Verilog (Circuit) (Configured skip)\n", + "[WARNING] Skipping Verilog (Circuit) (Configured skip)\n", + "2026-10-11 23:28:53,700 - src.data.multimodal - WARNING - Skipping Verilog (Circuit) (Configured skip)\n", + "[INFO] [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", + "[INFO] [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", + "2026-10-11 23:28:53,701 - src.data.multimodal - INFO - [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", + "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n", + "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n", + "2026-10-11 23:28:53,704 - src.data.multimodal - INFO - [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DEBUG: Loading 1 Arrow files directly...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[WARNING] Skipping Variant Effect (Coding) (Configured skip)\n", + "[WARNING] Skipping Variant Effect (Coding) (Configured skip)\n", + "2026-10-11 23:28:53,808 - src.data.multimodal - WARNING - Skipping Variant Effect (Coding) (Configured skip)\n", + "[WARNING] Skipping Variant Effect (Non-SNV) (Configured skip)\n", + "[WARNING] Skipping Variant Effect (Non-SNV) (Configured skip)\n", + "2026-10-11 23:28:53,808 - src.data.multimodal - WARNING - Skipping Variant Effect (Non-SNV) (Configured skip)\n", + "[INFO] [Init] Pre-Calculated Active Modalities: []\n", + "[INFO] [Init] Pre-Calculated Active Modalities: []\n", + "2026-10-11 23:28:53,809 - src.data.multimodal - INFO - [Init] Pre-Calculated Active Modalities: []\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[eval] stop token: '<|endoftext|>' (id 50279), pad id 1, max_new_tokens=128\n", + "[eval] 1/5 acc=0.000 (4s) gt='amyotrophic lateral sclerosis' pred='techtechtechtechtechtechtechtechtechtechtechtechtechtechtech'\n", + "[eval] 2/5 acc=0.000 (7s) gt='clear cell renal cell carcinoma' pred='ttttttttttttttttttttttttttttttt'\n", + "[eval] 3/5 acc=0.000 (9s) gt='melanoma' pred='ttttttttttttttttttttttttttttttt'\n", + "[eval] 4/5 acc=0.000 (11s) gt='alzheimers disease' pred='ttttttttttttttttttttttttttttttt'\n", + "[eval] 5/5 acc=0.000 (14s) gt='spinocerebellar ataxia' pred=''\n", + "{'accuracy': 0.0, 'format_rate': 0.0, 'n': 5}\n", + "TTTTTTt\n", + "Tt\n", + "Tt-Tttttt\n", + "Tttech\n", + "Tttechttechttechttechttechttechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtechtech\n", + "\n", + "\n", + "Answer: techtechtechtechtechtechtechtechtechtechtechtechtechtechte\n" + ] + } + ], + "source": [ + "from eval_kegg_aurora import load_eval_model, evaluate\n", + "ckpt = CKPT_SFT if Path(CKPT_SFT).exists() else (f\"{SMOKE_DIR}/checkpoints/step_final\" if Path(SMOKE_DIR).exists() else None)\n", + "print(\"checkpoint:\", ckpt)\n", + "m, ecfg = load_eval_model(ckpt, overrides=[f\"+model.dna_model_name={DNA_ENCODER}\"], device=dev)\n", + "quick = evaluate(m, ecfg, DS[\"kegg (test)\"], limit=5, max_new_tokens=128)\n", + "print({k: quick[k] for k in (\"accuracy\", \"format_rate\", \"n\")})\n", + "print(quick[\"records\"][0][\"completion\"][:500])\n", + "del m; torch.xpu.empty_cache()" + ] + }, + { + "cell_type": "markdown", + "id": "cell-069", + "metadata": {}, + "source": [ + "## 14. Troubleshooting\n", + "\n", + "| Symptom | Cause / fix |\n", + "|---|---|\n", + "| `SafetensorError … Disk quota exceeded` at the first save | Checkpoints went to `outputs/` under the home checkout. Pass `hydra.run.dir=` on `/flare` (all submit scripts now do, via `HYDRA_RUN_DIR`). |\n", + "| `cannot import name 'find_pruneable_heads_and_indices'` or `EsmConfig has no attribute is_decoder` | An NT-**v2** checkpoint under transformers 5. Use an NT v1 checkpoint (e.g. 2.5B multi-species) on `frameworks/2026.1.0`. |\n", + "| DNA `input_ids` clamped / out-of-range warnings | DNA tokenizer and encoder came from different NT releases. The tokenizer now follows `model.dna_model_name`, so keep them the same. |\n", + "| `torch_geometric` / IPEX import errors in the kernel | Venv activated before `module load frameworks`. Use the `prism-bioreason` kernel (its wrapper loads them in order). |\n", + "| `IndexError` in DDP side streams / \"device index out of range\" | Each rank must see one tile as `xpu:0`: `ZE_AFFINITY_MASK=$LOCAL_RANK` and `LOCAL_RANK=0` (the scripts' `mpiexec` block). |\n", + "| Training hangs at the first batch on compute nodes | Hub access attempted offline. Check `HF_HUB_OFFLINE=1` and that section 3 populated `$HF_HOME/hub` and `$PRISM_DATA_ROOT/dna_local_shards`. |\n", + "| GRPO job uses one tile only | By design: `ZoneDTrainer` is single-process (no DDP/vLLM yet). |\n", + "| `Merged sequence length 2798 … exceeds max_merged_seq_length=2048` | Set `MAX_SEQ_LENGTH=4096` (section 1; the GRPO/eval scripts default to it). Or lower `training.max_dna_length`. |\n", + "| Model echoes `Reference sequence: 5029250293`, or the DNA projector gets no gradient | Tag ids were rendered as text, so DNA was never spliced. Fixed in `_render_dna_prompt_text`. The section 10 assertion guards against it. |\n", + "| `ImportError: eval_kegg` during SFT | `training.eval_enabled=true` calls the not-yet-merged `eval_kegg.py`. Keep it `false` and use `eval_kegg_aurora.py`. |" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "PRISM BioReason (Aurora)", + "language": "python", + "name": "prism-bioreason" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/applications/bioreason/eval_kegg_aurora.py b/applications/bioreason/eval_kegg_aurora.py new file mode 100644 index 0000000..6beaefe --- /dev/null +++ b/applications/bioreason/eval_kegg_aurora.py @@ -0,0 +1,344 @@ +"""KEGG accuracy eval for BioReason checkpoints on Aurora (XPU), single tile. + +Stand-in until eval_kegg.py (PR #133) lands. It deliberately reuses the +training code paths instead of re-implementing them: + + * model: src.train.build_model_config + UnifiedTransformer, so the + architecture/special tokens match what src/train.py trained; + * weights: load_model_weights_only (base + projector from model.safetensors), + then merge_lora_into_base for the SFT adapter and, optionally, + a GRPO adapter stacked on top (GRPO checkpoints hold only + lora_adapter.pt -- see ZoneDTrainer.save_checkpoint); + * prompt: StreamingMultimodalDataset's own _process_dna_bioreason + + _render_dna_prompt_text in GRPO mode (is_sft=False), i.e. the + exact user turn + "<|im_start|>assistant\\n" generation prompt; + * decode: UnifiedTransformer.generate, as ZoneDTrainer._generate_completions. + +Usage (on a compute node, after `module load frameworks` + venv): + python applications/bioreason/eval_kegg_aurora.py \\ + --ckpt $PRISM_OUTPUT_ROOT/BIOREASON-SFT-KEGG//checkpoints/step_final \\ + [--grpo-adapter $PRISM_OUTPUT_ROOT/BIOREASON-GRPO-KEGG//checkpoints/step_final/lora_adapter.pt] \\ + --data $PRISM_DATA_ROOT/dna_local_shards_test/kegg \\ + --output results.json [--limit 50] [model=prism_olmo_1b ...extra Hydra overrides] +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import time +from collections import Counter + +REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +if REPO not in sys.path: + sys.path.insert(0, REPO) + +import torch # noqa: E402 + +ANSWER_RE = re.compile(r"(.*?)", re.DOTALL) + + +def pick_device() -> torch.device: + if hasattr(torch, "xpu") and torch.xpu.is_available(): + return torch.device("xpu") + if torch.cuda.is_available(): + return torch.device("cuda") + return torch.device("cpu") + + +def compose_cfg(overrides: list[str]): + """Hydra-compose src/conf/config.yaml exactly as src/train.py would.""" + from hydra import compose, initialize_config_dir + from hydra.core.global_hydra import GlobalHydra + + if GlobalHydra.instance().is_initialized(): + GlobalHydra.instance().clear() + base = ["model=prism_olmo_1b", "training=bioreason_grpo"] + with initialize_config_dir(config_dir=os.path.join(REPO, "src", "conf"), version_base=None): + return compose(config_name="config", overrides=base + list(overrides)) + + +def _adapter_alpha(path: str, fallback: float) -> float: + """Alpha the adapter was trained with (sidecar written by save_lora_adapter).""" + from src.utils.lora_utils import _load_adapter_meta + + meta = _load_adapter_meta(path) or {} + return float(meta.get("alpha") or fallback) + + +def load_eval_model( + ckpt: str | None, + grpo_adapter: str | None = None, + overrides: list[str] | None = None, + device: torch.device | None = None, +): + """Build the model the way src/train.py does and load trained weights. + + Returns (model, cfg). ``ckpt=None`` evaluates the untrained model (useful + as a baseline / smoke test). + """ + from src.model import UnifiedTransformer + from src.train import build_model_config + from src.training.distributed import load_model_weights_only + from src.utils.lora_utils import merge_lora_into_base + + # Merged length = text + 2 x max_dna_length DNA slots (~2.8k for KEGG), above + # the 2048 default of the merged-length guard (ModelConfig.max_merged_seq_length). + os.environ.setdefault("MAX_SEQ_LENGTH", "4096") + cfg = compose_cfg(overrides or []) + model = UnifiedTransformer(build_model_config(cfg)) + + if ckpt: + load_model_weights_only(model, ckpt, device="cpu") + sft_adapter = os.path.join(ckpt, "lora_adapter.pt") + if os.path.exists(sft_adapter): + alpha = _adapter_alpha(sft_adapter, cfg.training.get("resume_lora_alpha") or 64) + merge_lora_into_base(model.backbone, sft_adapter, alpha=alpha) + print(f"[eval] merged SFT adapter {sft_adapter} (alpha={alpha})") + if grpo_adapter: + alpha = _adapter_alpha(grpo_adapter, cfg.training.get("lora_alpha", 32)) + merge_lora_into_base(model.backbone, grpo_adapter, alpha=alpha) + print(f"[eval] merged GRPO adapter {grpo_adapter} (alpha={alpha})") + + device = device or pick_device() + model.to(device) + if model.backbone is not None and device.type != "cpu": + model.backbone.to(torch.bfloat16) + model.eval() + return model, cfg + + +DNA_MODES = ("real", "shuffled", "blank") +# Uninformative stand-in for --dna-mode blank. Non-empty on purpose: an empty string +# would give an all-zero DNA attention mask, which risks NaNs in the encoder softmax. +BLANK_DNA = "N" * 600 + + +def make_prompt_builder(model, cfg, dna_mode: str = "real"): + """Return ``build(item, donor=None) -> (prompt_batch, answer)`` using the dataset's renderer. + + dna_mode ablates only the DNA content (the prompt text and the answer stay real): + real -- the item's own reference/variant sequences + shuffled -- another item's sequences (``donor``), so DNA is plausible but unrelated + blank -- a constant uninformative sequence + """ + if dna_mode not in DNA_MODES: + raise ValueError(f"dna_mode must be one of {DNA_MODES}, got {dna_mode!r}") + from src.data.multimodal import StreamingMultimodalDataset + from src.train import ( + _resolve_dataset_overrides, + build_bio_dataset_kwargs, + build_training_config, + ) + + mc = model.config + tc = build_training_config(cfg, mc, output_dir="/tmp", seed=0) + helper = StreamingMultimodalDataset( + tokenizer=model.backbone_tokenizer, + model_config=mc, + dataset_overrides=_resolve_dataset_overrides(cfg), + **build_bio_dataset_kwargs("bioreason_grpo", tc, mc), + ) + tok = model.backbone_tokenizer + + def build(item, donor=None): + d = helper._process_dna_bioreason(item) + _, prompt_text = helper._render_dna_prompt_text(d["prompt"]) + ids = tok(prompt_text, return_tensors="pt", add_special_tokens=False).input_ids + if dna_mode == "shuffled": + if donor is None: + raise ValueError("dna_mode='shuffled' needs a donor item") + ref, var = helper._process_dna_bioreason(donor)["dna_sequences"] + elif dna_mode == "blank": + ref, var = BLANK_DNA, BLANK_DNA + else: + ref, var = d["dna_sequences"] + dna = {} + for key, seq in (("dna_reference", ref), ("dna_variant", var)): + enc = helper.dna_tokenizer( + seq, + return_tensors="pt", + padding="max_length", + truncation=True, + max_length=mc.max_dna_length, + ) + dna[key] = {"input_ids": enc["input_ids"], "attention_mask": enc["attention_mask"]} + batch = {"text": ids, "dna": dna, "_metadata": [f"{ids.shape[1]} 0"]} + return batch, d["answer"] + + return build + + +def extract_answer(completion: str) -> str: + from src.data.multimodal import clean_kegg_answer + + m = ANSWER_RE.search(completion) + return clean_kegg_answer(m.group(1) if m else completion) + + +def _to(batch, device): + if torch.is_tensor(batch): + return batch.to(device) + if isinstance(batch, dict): + return {k: _to(v, device) for k, v in batch.items()} + return batch + + +# Appended to a generation that never produced an tag, to force one (see _force_answer). +FORCE_SUFFIX = "\n\n\nAnswer: " +FORCE_MAX_NEW_TOKENS = 32 + + +def _force_answer(model, tok, device, batch, gen_ids, completion): + """Make a generation that ended without produce one. + + Re-prompts with prompt + the model's own generation (trailing eos dropped) + + FORCE_SUFFIX and decodes a few more tokens, so every example gets a scored + prediction. Callers record this separately (``forced``) so natural completion + stays visible in ``format_rate``. + """ + eos = tok.eos_token_id + gen = gen_ids.tolist() + while gen and gen[-1] == eos: + gen.pop() + suffix = tok(FORCE_SUFFIX, add_special_tokens=False, return_tensors="pt").input_ids.to(batch["text"].device) + ids = torch.cat([batch["text"], torch.tensor([gen], dtype=batch["text"].dtype, device=batch["text"].device), + suffix], dim=1) + forced = dict(batch, text=ids, _metadata=[f"{ids.shape[1]} 0"]) + with torch.no_grad(), torch.autocast(device_type=device.type, dtype=torch.bfloat16, + enabled=device.type != "cpu"): + out = model.generate(_to(forced, device), max_new_tokens=FORCE_MAX_NEW_TOKENS, do_sample=False, + pad_token_id=tok.pad_token_id, eos_token_id=eos) + cont = tok.decode(out[0, 1:], skip_special_tokens=True) + cont = cont.split("")[0].split("\n")[0] + return completion + FORCE_SUFFIX + cont + "" + + +def evaluate(model, cfg, dataset, limit: int | None = None, max_new_tokens: int = 1024, verbose=True, + dna_mode: str = "real"): + """Greedy-decode each KEGG item and score exact match on the span. + + Generation stops on OLMo's eos (<|endoftext|>, the token SFT supervises at the end of + the assistant turn) or at max_new_tokens. KEGG reasoning traces are 331-649 tokens + (+ answer), so 1024 leaves headroom; a smaller cap truncates before the answer. + """ + build = make_prompt_builder(model, cfg, dna_mode=dna_mode) + tok = model.backbone_tokenizer + if "im_end" in (tok.eos_token or ""): + raise ValueError(f"eos must be OLMo's <|endoftext|>, not a Qwen ChatML token: {tok.eos_token!r}") + print(f"[eval] stop token: {tok.eos_token!r} (id {tok.eos_token_id}), pad id {tok.pad_token_id}, " + f"max_new_tokens={max_new_tokens}", flush=True) + device = next(model.parameters()).device + n = len(dataset) if limit is None else min(limit, len(dataset)) + records = [] + t0 = time.time() + for i in range(n): + # shuffled: donor = next item in the full split (wraps), a deterministic derangement + donor = dataset[(i + 1) % len(dataset)] if dna_mode == "shuffled" else None + batch, answer = build(dataset[i], donor) + with torch.no_grad(), torch.autocast(device_type=device.type, dtype=torch.bfloat16, + enabled=device.type != "cpu"): + out = model.generate( + _to(batch, device), + max_new_tokens=max_new_tokens, + do_sample=False, + pad_token_id=tok.pad_token_id, + eos_token_id=tok.eos_token_id, + ) + completion = tok.decode(out[0, 1:], skip_special_tokens=True) # strip BOS seed + natural = bool(ANSWER_RE.search(completion)) + if not natural: + completion = _force_answer(model, tok, device, batch, out[0, 1:], completion) + pred = extract_answer(completion) + records.append({ + "idx": i, + "answer": answer, + "pred": pred, + "correct": pred == answer, + "has_answer_tag": natural, # the model's own generation, before any forcing + "forced": not natural, + "completion": completion, + }) + if verbose: + acc = sum(r["correct"] for r in records) / len(records) + print(f"[eval] {i + 1}/{n} acc={acc:.3f} ({time.time() - t0:.0f}s) " + f"gt={answer!r} pred={pred[:60]!r}", flush=True) + return summarize(records) + + +def _prf(records: list[dict]) -> dict: + """Macro/weighted precision, recall, F1 over the ground-truth labels present.""" + support = Counter(r["answer"] for r in records) + tp, fp = Counter(), Counter() + for r in records: + if r["correct"]: + tp[r["answer"]] += 1 + else: + fp[r["pred"]] += 1 + per, n = {}, max(len(records), 1) + for lab, sup in support.items(): + p = tp[lab] / (tp[lab] + fp[lab]) if (tp[lab] + fp[lab]) else 0.0 + rc = tp[lab] / sup + per[lab] = (p, rc, 2 * p * rc / (p + rc) if (p + rc) else 0.0, sup) + k = max(len(per), 1) + return { + "precision_macro": sum(v[0] for v in per.values()) / k, + "recall_macro": sum(v[1] for v in per.values()) / k, + "f1_macro": sum(v[2] for v in per.values()) / k, + "f1_weighted": sum(v[2] * v[3] for v in per.values()) / n, + } + + +def summarize(records: list[dict]) -> dict: + n = len(records) + per_label = Counter(r["answer"] for r in records) + per_label_correct = Counter(r["answer"] for r in records if r["correct"]) + return { + "n": n, + "accuracy": sum(r["correct"] for r in records) / max(n, 1), + "format_rate": sum(r["has_answer_tag"] for r in records) / max(n, 1), + "forced_answer_rate": sum(r.get("forced", False) for r in records) / max(n, 1), + # accuracy of always predicting the most common label; real accuracy is only + # meaningful relative to this (KEGG is heavily imbalanced) + "majority_baseline": (per_label.most_common(1)[0][1] / n) if n else 0.0, + **_prf(records), + "per_label": { + k: {"n": v, "correct": per_label_correct[k]} for k, v in per_label.most_common() + }, + "records": records, + } + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--ckpt", help="SFT (or projector) checkpoint dir containing model.safetensors") + ap.add_argument("--grpo-adapter", help="GRPO lora_adapter.pt to merge on top of --ckpt") + ap.add_argument("--data", required=True, help="KEGG split saved with save_to_disk (helpers/download_bioreason_dna_datasets.py)") + ap.add_argument("--output", required=True, help="JSON file for metrics + per-example records") + ap.add_argument("--limit", type=int, default=None) + ap.add_argument("--max-new-tokens", type=int, default=1024) + ap.add_argument("--dna-mode", choices=DNA_MODES, default="real", + help="DNA ablation: real, shuffled (another item's DNA), or blank (constant N)") + args, overrides = ap.parse_known_args() + + from datasets import load_from_disk + + ds = load_from_disk(args.data) + model, cfg = load_eval_model(args.ckpt, args.grpo_adapter, overrides) + res = evaluate(model, cfg, ds, limit=args.limit, max_new_tokens=args.max_new_tokens, + dna_mode=args.dna_mode) + res.update({"ckpt": args.ckpt, "grpo_adapter": args.grpo_adapter, "data": args.data, + "dna_mode": args.dna_mode}) + os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True) + with open(args.output, "w") as f: + json.dump(res, f, indent=1) + print(f"[eval] dna={args.dna_mode} accuracy={res['accuracy']:.4f} f1_macro={res['f1_macro']:.4f} " + f"majority={res['majority_baseline']:.4f} n={res['n']} " + f"format_rate={res['format_rate']:.3f} forced={res['forced_answer_rate']:.3f} -> {args.output}") + + +if __name__ == "__main__": + main() diff --git a/applications/bioreason/flow-diag.png b/applications/bioreason/flow-diag.png new file mode 100644 index 0000000000000000000000000000000000000000..3725c21ab789afd82cec6823f861abababfb4c5e GIT binary patch literal 276630 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

YW&- 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 literal 0 HcmV?d00001 diff --git a/applications/bioreason/helpers/demo_helpers.py b/applications/bioreason/helpers/demo_helpers.py new file mode 100644 index 0000000..3f5ef45 --- /dev/null +++ b/applications/bioreason/helpers/demo_helpers.py @@ -0,0 +1,506 @@ +"""Display + PBS plumbing for bioreason_demo.ipynb. + +Everything PRISM-specific (configs, model, data, tokens) lives in the notebook +itself so it can be read top to bottom; this module only holds the +presentation helpers (HTML renderers, plots, widgets) and the qsub/qstat +wrappers that would otherwise bury the interesting cells. +""" + +from __future__ import annotations + +import difflib +import html +import json +import re +import subprocess +from collections import Counter +from pathlib import Path + +# --------------------------------------------------------------------------- +# Palette (validated reference categorical slots 1-3 + neutral inks) +# --------------------------------------------------------------------------- +SERIES = ["#2a78d6", "#eb6834", "#1baf7a"] # blue, orange, aqua +INK = "#0b0b0b" +INK_2 = "#52514e" +GRID = "#e4e3df" +# Nucleotide colors (A/C/G/T) for sequence rendering -- identity only, always +# shown next to the letter itself so color is never the sole carrier. +BASE_COLORS = {"A": "#1baf7a", "C": "#2a78d6", "G": "#eda100", "T": "#e34948", "N": "#8a8984"} + + +def style_ax(ax, title=None, xlabel=None, ylabel=None): + """Recessive axes: no top/right spines, light y-grid, muted tick ink.""" + for side in ("top", "right"): + ax.spines[side].set_visible(False) + for side in ("left", "bottom"): + ax.spines[side].set_color(GRID) + ax.tick_params(colors=INK_2, labelsize=9) + ax.grid(axis="y", color=GRID, linewidth=0.8) + ax.set_axisbelow(True) + if title: + ax.set_title(title, loc="left", fontsize=11, color=INK) + if xlabel: + ax.set_xlabel(xlabel, color=INK_2, fontsize=9) + if ylabel: + ax.set_ylabel(ylabel, color=INK_2, fontsize=9) + return ax + + +# --------------------------------------------------------------------------- +# Source excerpts +# --------------------------------------------------------------------------- +def show_source(path: str | Path, start: int | None = None, end: int | None = None, + pattern: str | None = None, context: int = 25, repo: str | Path = "."): + """Render a numbered excerpt of a repo file as a Markdown code block. + + Either give explicit ``start``/``end`` lines or a regex ``pattern``: the + excerpt then starts at its first match and spans ``context`` lines, so the + notebook does not break when line numbers drift. + """ + from IPython.display import Markdown, display + + p = Path(repo) / path + lines = p.read_text().splitlines() + if pattern is not None: + rx = re.compile(pattern) + hit = next((i for i, l in enumerate(lines) if rx.search(l)), None) + if hit is None: + raise ValueError(f"{pattern!r} not found in {path}") + start, end = hit + 1, min(len(lines), hit + context) + start = start or 1 + end = end or len(lines) + body = "\n".join(f"{i:5d} {lines[i - 1]}" for i in range(start, end + 1)) + lang = {".py": "python", ".json": "json", ".yaml": "yaml", ".sh": "bash"}.get(p.suffix, "") + display(Markdown(f"`{path}:{start}-{end}`\n```{lang}\n{body}\n```")) + + +# --------------------------------------------------------------------------- +# DNA sample rendering +# --------------------------------------------------------------------------- +def diff_spans(ref: str, var: str): + """Opcodes (tag, i1, i2, j1, j2) where ref and var differ (SNV / indel).""" + sm = difflib.SequenceMatcher(None, ref, var, autojunk=False) + return [op for op in sm.get_opcodes() if op[0] != "equal"] + + +def _bases_html(seq: str, offset: int, highlight: set[int]) -> str: + out = [] + for k, ch in enumerate(seq): + c = BASE_COLORS.get(ch.upper(), BASE_COLORS["N"]) + style = f"color:{c};" + if offset + k in highlight: + style += "background:#fde68a;font-weight:700;outline:1px solid #b45309;" + out.append(f'{html.escape(ch)}') + return "".join(out) + + +def dna_pair_html(ref: str, var: str, window: int = 120) -> str: + """Ref vs variant around the first difference, differing bases highlighted.""" + ops = diff_spans(ref, var) + if ops: + _, i1, i2, j1, j2 = ops[0] + else: + i1 = i2 = j1 = j2 = len(ref) // 2 + a0, a1 = max(0, i1 - window), min(len(ref), i2 + window) + b0, b1 = max(0, j1 - window), min(len(var), j2 + window) + ref_hl = {i for op in ops for i in range(op[1], op[2])} + var_hl = {j for op in ops for j in range(op[3], op[4])} + kinds = Counter(op[0] for op in ops) + summary = ", ".join(f"{v} {k}" for k, v in kinds.items()) or "identical" + mono = "font-family:ui-monospace,Menlo,monospace;font-size:12px;word-break:break-all;line-height:1.5" + return ( + f'

' + ) + + +def sample_html(item: dict) -> str: + ref = item.get("reference_sequence", "") + var = item.get("variant_sequence") or item.get("mutated_sequence", "") + parts = [dna_pair_html(ref, var)] + for key in ("question", "reasoning", "answer"): + if item.get(key): + text = html.escape(str(item[key])) + if key == "reasoning" and len(text) > 1500: + text = text[:1500] + " …" + parts.append(f'

{key}
{text}

') + return "".join(parts) + + +def sample_browser(datasets: dict): + """Dropdown (dataset) + slider (row) -> rendered sample.""" + import ipywidgets as w + from IPython.display import HTML, display + + names = list(datasets) + dd = w.Dropdown(options=names, description="dataset") + sl = w.IntSlider(min=0, max=len(datasets[names[0]]) - 1, description="row", + continuous_update=False, layout=w.Layout(width="60%")) + out = w.Output() + + def render(*_): + sl.max = len(datasets[dd.value]) - 1 + with out: + out.clear_output(wait=True) + display(HTML(sample_html(datasets[dd.value][sl.value]))) + + dd.observe(render, "value") + sl.observe(render, "value") + render() + display(w.VBox([w.HBox([dd, sl]), out])) + + +# --------------------------------------------------------------------------- +# Dataset statistics +# --------------------------------------------------------------------------- +def length_frame(datasets: dict, llm_tok, dna_tok, max_rows: int = 2000): + """Per-row lengths: bp, DNA tokens (6-mer NT), question/answer/reasoning LLM tokens.""" + import pandas as pd + + rows = [] + for name, ds in datasets.items(): + n = min(len(ds), max_rows) + sub = ds.select(range(n)) + for item in sub: + var = item.get("variant_sequence") or item.get("mutated_sequence", "") + rows.append({ + "dataset": name, + "ref_bp": len(item["reference_sequence"]), + "var_bp": len(var), + "ref_dna_tokens": len(dna_tok(item["reference_sequence"]).input_ids), + "question_tokens": len(llm_tok(item.get("question", "")).input_ids), + "answer_tokens": len(llm_tok(str(item.get("answer", ""))).input_ids), + "reasoning_tokens": len(llm_tok(item.get("reasoning", "") or "").input_ids), + }) + return pd.DataFrame(rows) + + +def plot_lengths(df, max_dna_length: int = 1024): + """DNA span length vs the fixed padding, plus text-length histograms (one measure per panel).""" + import matplotlib.pyplot as plt + + fig, axes = plt.subplots(1, 4, figsize=(17, 3.4), gridspec_kw={"width_ratios": [1.2, 1, 1, 1]}) + names = list(df["dataset"].unique()) + + # Panel 1: DNA tokens are ~constant per dataset (fixed-length windows), so a + # bar per dataset against the padded span length says more than a histogram. + ax = axes[0] + med = df.groupby("dataset", sort=False)["ref_dna_tokens"].max().reindex(names) + ax.barh(names[::-1], med.values[::-1], color=SERIES[0], height=0.55) + ax.axvline(max_dna_length, color=INK, linestyle="--", linewidth=1) + for y, v in enumerate(med.values[::-1]): + ax.text(v, y, f" {v:.0f} tok ({1 - v / max_dna_length:.0%} padding)", va="center", + fontsize=8, color=INK) + ax.set_xlim(0, max_dna_length * 1.08) + style_ax(ax, title=f"DNA tokens per span (max) vs max_dna_length={max_dna_length}") + ax.grid(axis="y", visible=False) + ax.grid(axis="x", color=GRID, linewidth=0.8) + + panels = [ + ("question_tokens", "Question tokens (OLMo)"), + ("reasoning_tokens", "Reasoning tokens (OLMo, KEGG only)"), + ("answer_tokens", "Answer tokens (OLMo)"), + ] + for ax, (col, title) in zip(axes[1:], panels): + for i, name in enumerate(names): + vals = df.loc[df.dataset == name, col] + if vals.max() == 0: + continue + ax.hist(vals, bins=40, alpha=0.6, color=SERIES[i % 3], label=name, + edgecolor="white", linewidth=0.5) + style_ax(ax, title=title, ylabel="rows" if col == "question_tokens" else None) + axes[1].legend(frameon=False, fontsize=8) + fig.tight_layout() + return fig + + +def plot_counts(counts: dict, title: str): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(6, 0.45 * len(counts) + 0.8)) + names = list(counts)[::-1] + vals = [counts[n] for n in names] + ax.barh(names, vals, color=SERIES[0], height=0.6) + for y, v in enumerate(vals): + ax.text(v, y, f" {v:,}", va="center", fontsize=9, color=INK) + style_ax(ax, title=title) + ax.grid(axis="x", color=GRID, linewidth=0.8) + ax.grid(axis="y", visible=False) + fig.tight_layout() + return fig + + +# --------------------------------------------------------------------------- +# Model introspection +# --------------------------------------------------------------------------- +def module_tree(model, max_depth: int = 2) -> str: + """Compact module tree: name, class, #params, down to ``max_depth``.""" + lines = [] + + def walk(mod, name, depth): + n = sum(p.numel() for p in mod.parameters()) + lines.append(f"{' ' * depth}{name or ''}: {type(mod).__name__} ({n / 1e6:,.1f}M)") + if depth < max_depth: + for child_name, child in mod.named_children(): + walk(child, child_name, depth + 1) + + walk(model, "", 0) + return "\n".join(lines) + + +def _group(name: str) -> str: + if "lora_" in name: + return "LoRA adapters" + if name.startswith("encoders.dna.proj"): + return "DNA encoder output Linear (2560->d_dna)" + if name.startswith("encoders.dna"): + return "DNA encoder (NT)" + if name.startswith("projectors.dna"): + return "DNA projector (MLP)" + if name.startswith("projectors"): + return "other projectors" + if "embed_tokens" in name or "lm_head" in name: + return "LLM embeddings / lm_head" + if name.startswith("backbone"): + return "LLM backbone" + return "other" + + +def param_table(model): + """Total / trainable parameters per component.""" + import pandas as pd + + agg: dict[str, list[int]] = {} + for n, p in model.named_parameters(): + g = agg.setdefault(_group(n), [0, 0]) + g[0] += p.numel() + g[1] += p.numel() if p.requires_grad else 0 + df = pd.DataFrame( + [(k, v[0], v[1]) for k, v in agg.items()], columns=["component", "params", "trainable"] + ).set_index("component") + df.loc["TOTAL"] = df.sum() + df["trainable %"] = (100 * df.trainable / df.params.clip(lower=1)).round(2) + return df + + +def side_by_side(*frames, titles=()): + from IPython.display import HTML, display + + cells = [] + for i, f in enumerate(frames): + t = titles[i] if i < len(titles) else "" + tbl = f.style.format({"params": "{:,.0f}", "trainable": "{:,.0f}"}).to_html() + cells.append(f'
{html.escape(t)}{tbl}
') + display(HTML(f'
{"".join(cells)}
')) + + +# --------------------------------------------------------------------------- +# Tokenization view +# --------------------------------------------------------------------------- +def token_view(tokenizer, ids, prompt_len: int | None, special: dict[int, str], + dna_tokens_per_span: int) -> str: + """Render token ids: prompt (masked) vs target (loss) and DNA splice slots.""" + out = [] + for i, t in enumerate(ids): + t = int(t) + if t in special: + label = html.escape(special[t]) + extra = (f' [{dna_tokens_per_span} DNA embeddings]' + if special[t].endswith("_start>") else "") + out.append(f'{label}{extra}') + continue + piece = html.escape(tokenizer.decode([t])) + in_target = prompt_len is not None and i >= prompt_len + style = ("background:#dcfce7;" if in_target else f"color:{INK_2};") + out.append(f'{piece}') + legend = (f'
' + f'grey = prompt (label -100, no loss)   ' + f'green = target (loss)   ' + f'blue = DNA splice tag
') + return (legend + '
' + "".join(out) + "
") + + +# --------------------------------------------------------------------------- +# PBS: patch, submit, monitor +# --------------------------------------------------------------------------- +def patch_stage_script(src: str | Path, dst: str | Path) -> Path: + """Copy a submit_bioreason_{projector,sft}.sh with two notebook-side fixes. + + * Checkpoints go to ``$HYDRA_RUN_DIR`` (on /flare) instead of Hydra's + default ``outputs/`` under the home-directory checkout, which hit + "Disk quota exceeded" at the first save. ``$EXTRA_HYDRA`` is appended + verbatim for any further overrides. + * Drops the repo ``.venv`` activation (a CUDA torch build) that the + projector script layers on top of the XPU venv. + + Idempotent: a script that already passes ``hydra.run.dir`` (and defines + ``HYDRA_RUN_DIR``) is only stripped of the ``.venv`` line. The originals + are left untouched. + """ + text = Path(src).read_text() + text = text.replace('source "${PROJ_DIR}/.venv/bin/activate"\n', "") + if "hydra.run.dir=" not in text: + last_arg = " wandb.mode='\"${WANDB_MODE}\"'\n'" + if last_arg not in text: + raise ValueError(f"{src}: could not find the train.py argument list to extend") + text = text.replace( + last_arg, + " wandb.mode='\"${WANDB_MODE}\"' \\\n" + " hydra.run.dir='\"${HYDRA_RUN_DIR}\"' '\"${EXTRA_HYDRA:-}\"'\n'", + ) + if not re.search(r"^HYDRA_RUN_DIR=", text, re.M): + # Must exist in the outer shell before the mpiexec string is built. + text = text.replace( + 'cd "${PROJ_DIR}"\n', + 'HYDRA_RUN_DIR="${HYDRA_RUN_DIR:-${PRISM_OUTPUT_ROOT}/${RUN_ID}/${RUN_TAG:-$(date +%Y%m%d-%H%M%S)}}"\n' + 'cd "${PROJ_DIR}"\n', + 1, + ) + dst = Path(dst) + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(text) + dst.chmod(0o755) + return dst + + +def qsub(script, env: dict | None = None, depend: str | None = None, log: str | None = None, + queue: str | None = None, walltime: str | None = None, name: str | None = None, + cwd: str | None = None, dry_run: bool = False) -> str: + """Submit ``script`` and return the job id. ``env`` is passed with -v.""" + cmd = ["qsub"] + if env: + bad = [k for k, v in env.items() if "," in str(v)] + if bad: + raise ValueError(f"qsub -v values cannot contain commas: {bad}") + cmd += ["-v", ",".join(f"{k}={v}" for k, v in env.items())] + if depend: + cmd += ["-W", f"depend=afterok:{depend}"] + if log: + Path(log).parent.mkdir(parents=True, exist_ok=True) + cmd += ["-o", str(log)] + if queue: + cmd += ["-q", queue] + if walltime: + cmd += ["-l", f"walltime={walltime}"] + if name: + cmd += ["-N", name] + cmd.append(str(script)) + print(" ".join(cmd)) + if dry_run: + return "DRYRUN" + res = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd) + if res.returncode != 0: + raise RuntimeError(res.stderr.strip() or res.stdout.strip()) + return res.stdout.strip() + + +def qstat_table(jobids): + """State of each job (works for finished jobs too via -x).""" + import pandas as pd + + ids = [j for j in jobids if j and j != "DRYRUN"] + if not ids: + return pd.DataFrame() + res = subprocess.run(["qstat", "-x", "-f", "-F", "json", *ids], capture_output=True, text=True) + try: + jobs = json.loads(res.stdout).get("Jobs", {}) + except json.JSONDecodeError: + print(res.stderr or res.stdout) + return pd.DataFrame() + state = {"Q": "queued", "R": "running", "H": "held", "F": "finished", "E": "exiting", "B": "array"} + rows = [] + for jid, j in jobs.items(): + rows.append({ + "job": jid.split(".")[0], + "name": j.get("Job_Name"), + "queue": j.get("queue"), + "state": state.get(j.get("job_state"), j.get("job_state")), + "exit": j.get("Exit_status", ""), + "walltime used": j.get("resources_used", {}).get("walltime", ""), + "depends on": (j.get("depend") or "").replace("afterok:", "").split(".")[0], + }) + return pd.DataFrame(rows).set_index("job") + + +def tail(path, n: int = 30) -> str: + p = Path(path) + if not p.exists(): + return f"(no log yet: {p})" + return "\n".join(p.read_text(errors="replace").splitlines()[-n:]) + + +_ZONE_A = re.compile(r"Step (\d+): Loss ([-\d.]+)") +_GRPO = re.compile(r"step=(\d+) loss=([-\d.]+) avg_reward=([-\d.]+)") + + +def parse_log(path) -> dict: + """{'step': [...], 'loss': [...], 'reward': [...]} from a train.py log.""" + out = {"step": [], "loss": [], "reward": []} + p = Path(path) + if not p.exists(): + return out + for line in p.read_text(errors="replace").splitlines(): + m = _GRPO.search(line) + if m: + out["step"].append(int(m[1])) + out["loss"].append(float(m[2])) + out["reward"].append(float(m[3])) + continue + m = _ZONE_A.search(line) + if m: + out["step"].append(int(m[1])) + out["loss"].append(float(m[2])) + return out + + +def plot_logs(logs: dict): + """One small multiple per run (each its own y-scale; never a dual axis).""" + import matplotlib.pyplot as plt + + runs = {k: parse_log(v) for k, v in logs.items()} + panels = [(k, "loss", r) for k, r in runs.items()] + panels += [(k, "reward", r) for k, r in runs.items() if r["reward"]] + fig, axes = plt.subplots(1, len(panels), figsize=(4.2 * len(panels), 3), squeeze=False) + for ax, (name, key, r) in zip(axes[0], panels): + if r[key]: + ax.plot(r["step"], r[key], color=SERIES[0] if key == "loss" else SERIES[2], linewidth=2) + ax.annotate(f"{r[key][-1]:.3f}", (r["step"][-1], r[key][-1]), fontsize=9, color=INK, + xytext=(4, 0), textcoords="offset points", va="center") + style_ax(ax, title=f"{name}: {key}", xlabel="step") + else: + ax.set_axis_off() + ax.set_title(f"{name}: {key}", loc="left", fontsize=11, color=INK) + ax.text(0.5, 0.5, "no steps logged yet", ha="center", va="center", + transform=ax.transAxes, color=INK_2) + fig.tight_layout() + return fig + + +def monitor(jobids, logs: dict, n_tail: int = 15): + """Refresh button: qstat table + log tails + loss curves.""" + import ipywidgets as w + import matplotlib.pyplot as plt + from IPython.display import display + + btn = w.Button(description="Refresh", icon="refresh") + out = w.Output() + + def refresh(*_): + with out: + out.clear_output(wait=True) + display(qstat_table(jobids)) + fig = plot_logs(logs) + display(fig) + plt.close(fig) + for name, path in logs.items(): + print(f"--- {name}: {path}") + print(tail(path, n_tail)) + + btn.on_click(refresh) + refresh() + display(w.VBox([btn, out])) diff --git a/applications/bioreason/download_bioreason_dna_datasets.py b/applications/bioreason/helpers/download_bioreason_dna_datasets.py similarity index 78% rename from applications/bioreason/download_bioreason_dna_datasets.py rename to applications/bioreason/helpers/download_bioreason_dna_datasets.py index aad459f..2fce5fe 100644 --- a/applications/bioreason/download_bioreason_dna_datasets.py +++ b/applications/bioreason/helpers/download_bioreason_dna_datasets.py @@ -4,10 +4,10 @@ branch) can pick them up without hitting the HF Hub at train time. Usage: - python applications/bioreason/download_bioreason_dna_datasets.py --dataset kegg --output-dir /path/to/output - python applications/bioreason/download_bioreason_dna_datasets.py --dataset variant_effect_coding --output-dir /path/to/output - python applications/bioreason/download_bioreason_dna_datasets.py --dataset variant_effect_non_snv --output-dir /path/to/output - python applications/bioreason/download_bioreason_dna_datasets.py --dataset all --output-dir /path/to/output_root + python applications/bioreason/helpers/download_bioreason_dna_datasets.py --dataset kegg --output-dir /path/to/output + python applications/bioreason/helpers/download_bioreason_dna_datasets.py --dataset variant_effect_coding --output-dir /path/to/output + python applications/bioreason/helpers/download_bioreason_dna_datasets.py --dataset variant_effect_non_snv --output-dir /path/to/output + python applications/bioreason/helpers/download_bioreason_dna_datasets.py --dataset all --output-dir /path/to/output_root """ import argparse diff --git a/applications/bioreason/helpers/kernel_wrapper.sh b/applications/bioreason/helpers/kernel_wrapper.sh new file mode 100755 index 0000000..2cc5adb --- /dev/null +++ b/applications/bioreason/helpers/kernel_wrapper.sh @@ -0,0 +1,28 @@ +#!/bin/bash -l +# Jupyter kernel launcher for the BioReason demo notebook (Aurora). +# +# Registered by setup_jupyter_kernel.sh as the argv of the `prism-bioreason` +# kernel. Jupyter/VS Code start kernels from a bare environment, so the +# frameworks module and the PRISM venv have to be loaded here, in the same +# order the scripts/training/submit_bioreason_*.sh scripts use (module first, then venv -- +# reversing it breaks torch/IPEX imports). +# +# Do not call directly; Jupyter passes `-f ` as "$@". + +FRAMEWORKS_MODULE="${FRAMEWORKS_MODULE:-frameworks/2026.1.0}" +PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" + +module use /soft/modulefiles +module load "${FRAMEWORKS_MODULE}" +module load hdf5 +source "${PRISM_VENV}/bin/activate" + +# One PVC tile per device (12 per node), matching the submit scripts. +export ZE_FLAT_DEVICE_HIERARCHY=FLAT +export ZE_ENABLE_PCI_ID_DEVICE_ORDER=1 +# PBS's default TMPDIR overflows the AF_UNIX socket path limit used by +# DataLoader worker IPC (see submit_bioreason_sft.sh). +export TMPDIR=/tmp +export PYTHONWARNINGS=ignore + +exec python -m ipykernel_launcher "$@" diff --git a/applications/bioreason/helpers/setup_jupyter_kernel.sh b/applications/bioreason/helpers/setup_jupyter_kernel.sh new file mode 100755 index 0000000..6c25ee5 --- /dev/null +++ b/applications/bioreason/helpers/setup_jupyter_kernel.sh @@ -0,0 +1,64 @@ +#!/bin/bash -l +# One-time setup for applications/bioreason/bioreason_demo.ipynb on Aurora. +# +# 1. Builds (or reuses) the BioReason venv on top of frameworks/2026.1.0 -- +# the same venv the scripts/training/submit_bioreason_*.sh scripts activate. +# 2. Installs the notebook-only extras (ipykernel, ipywidgets) plus the +# training extras the frameworks module lacks. +# 3. Registers a Jupyter kernel `prism-bioreason` whose argv is +# kernel_wrapper.sh, so the kernel always starts with the module + venv +# loaded (VS Code Remote and jupyter lab both pick it up). +# +# Run once from a login node (UAN; it has direct internet access): +# bash applications/bioreason/helpers/setup_jupyter_kernel.sh +# Then, from VS Code connected to the interactive compute node, open the +# notebook and pick the "PRISM BioReason (Aurora)" kernel. + +set -eo pipefail + +HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PRISM_DIR="$( cd "${HERE}/../../.." && pwd )" +FRAMEWORKS_MODULE="${FRAMEWORKS_MODULE:-frameworks/2026.1.0}" +PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +KERNEL_NAME="prism-bioreason" + +module use /soft/modulefiles +module load "${FRAMEWORKS_MODULE}" + +if ! command -v uv >/dev/null 2>&1; then + echo "ERROR: uv is not on PATH. Install: curl -LsSf https://astral.sh/uv/install.sh | sh" + exit 1 +fi + +if [ ! -f "${PRISM_VENV}/bin/activate" ]; then + echo "Creating ${PRISM_VENV} (--system-site-packages over ${FRAMEWORKS_MODULE})" + uv venv --system-site-packages -p "$(which python)" "${PRISM_VENV}" +fi +source "${PRISM_VENV}/bin/activate" + +# Same extras as the recipe in submit_bioreason_sft.sh; --no-deps keeps the +# module's XPU torch from being replaced by a PyPI/CUDA wheel. +uv pip install wandb braceexpand==0.1.7 +uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 +# jupyter_server: fallback when VS Code doesn't list the kernel -- run +# `jupyter server --no-browser --ip=$(hostname) --port 8888` on the compute node +# and connect via "Select Kernel -> Existing Jupyter Server". +uv pip install ipykernel ipywidgets jupyter_server +uv pip install --no-deps -e "${PRISM_DIR}" + +chmod +x "${HERE}/kernel_wrapper.sh" +KERNEL_DIR="${HOME}/.local/share/jupyter/kernels/${KERNEL_NAME}" +mkdir -p "${KERNEL_DIR}" +cat > "${KERNEL_DIR}/kernel.json" < ${KERNEL_DIR}/kernel.json" +echo " venv: ${PRISM_VENV}" +echo " module: ${FRAMEWORKS_MODULE}" diff --git a/applications/bioreason/helpers/summarize_eval_sweep.py b/applications/bioreason/helpers/summarize_eval_sweep.py new file mode 100644 index 0000000..6a8cfe8 --- /dev/null +++ b/applications/bioreason/helpers/summarize_eval_sweep.py @@ -0,0 +1,37 @@ +"""Print a table from a submit_eval_sft_sweep.sh output directory of eval_kegg_aurora.py JSONs. + +Usage: python applications/bioreason/helpers/summarize_eval_sweep.py +Stdlib only. Compare the real row against the shuffled/blank rows and the majority baseline. +""" + +import glob +import json +import os +import re +import sys + + +def step_key(name: str): + m = re.match(r"step_(\d+|final)_", name) + return (1, 0) if m and m.group(1) == "final" else (0, int(m.group(1)) if m else -1) + + +def main(d: str): + rows = [] + for p in glob.glob(os.path.join(d, "*.json")): + r = json.load(open(p)) + rows.append((os.path.basename(p)[:-5], r)) + rows.sort(key=lambda x: (step_key(x[0]), x[0])) + cols = ["acc", "f1_macro", "f1_wtd", "majority", "format", "n"] + print(f"{'run':<26}{'dna':<10}" + "".join(f"{c:>10}" for c in cols)) + for name, r in rows: + vals = [r["accuracy"], r.get("f1_macro", float("nan")), r.get("f1_weighted", float("nan")), + r.get("majority_baseline", float("nan")), r["format_rate"], r["n"]] + print(f"{name:<26}{r.get('dna_mode', 'real'):<10}" + + "".join(f"{v:>10.4f}" if isinstance(v, float) else f"{v:>10}" for v in vals)) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit(__doc__) + main(sys.argv[1]) diff --git a/applications/bioreason/submit_eval_kegg.sh b/applications/bioreason/scripts/eval/submit_eval_kegg.sh similarity index 100% rename from applications/bioreason/submit_eval_kegg.sh rename to applications/bioreason/scripts/eval/submit_eval_kegg.sh diff --git a/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh b/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh new file mode 100755 index 0000000..fe68aa9 --- /dev/null +++ b/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh @@ -0,0 +1,94 @@ +#!/bin/bash -l +#PBS -N bioreason-eval-kegg-1b +#PBS -l select=1 +#PBS -l walltime=01:00:00 +#PBS -l filesystems=home:flare +#PBS -q debug +#PBS -A ModCon +#PBS -k doe +#PBS -j oe +# +# Aurora XPU launcher: KEGG accuracy eval for BioReason checkpoints (1 tile). +# Aurora port of submit_eval_kegg.sh (Polaris); runs eval_kegg_aurora.py +# because eval_kegg.py (PR #133) is not in this repo yet. +# +# CKPT SFT checkpoint dir (model.safetensors [+ lora_adapter.pt]). Required. +# GRPO_ADAPTER optional GRPO lora_adapter.pt merged on top of CKPT. +# SPLIT KEGG split to score (default test; saved by the notebook / +# helpers/download_bioreason_dna_datasets.py under dna_local_shards_/kegg). +# LIMIT optional cap on examples. +# DNA_MODE real (default) | shuffled (another item's DNA) | blank (constant N): DNA ablation. +# OUTPUT metrics JSON (default ${PRISM_OUTPUT_ROOT}/BIOREASON-EVAL-KEGG/${RUN_TAG}/results.json, +# or results_.json when DNA_MODE != real). +# +# qsub -v CKPT=/checkpoints/step_final applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh +# qsub -v CKPT=...,GRPO_ADAPTER=/checkpoints/step_final/lora_adapter.pt,LIMIT=50 ... + +set -eo pipefail + +PROJ_DIR="${PROJ_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." 2>/dev/null && pwd)}" +if [ -n "${PBS_O_WORKDIR:-}" ] && [ ! -f "${PROJ_DIR}/src/train.py" ]; then + PROJ_DIR="${PBS_O_WORKDIR}" +fi +RUN_TAG="${RUN_TAG:-$(date +%Y%m%d-%H%M%S)}" +SPLIT="${SPLIT:-test}" +MAX_NEW_TOKENS="${MAX_NEW_TOKENS:-1024}" +DNA_MODE="${DNA_MODE:-real}" +case "${DNA_MODE}" in real|shuffled|blank) ;; *) echo "ERROR: DNA_MODE must be real|shuffled|blank"; exit 1;; esac + +export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects/ModCon/${USER}/cache}" +export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/${USER}/output}" +RESULT_NAME="results.json"; [ "${DNA_MODE}" != "real" ] && RESULT_NAME="results_${DNA_MODE}.json" +OUTPUT="${OUTPUT:-${PRISM_OUTPUT_ROOT}/BIOREASON-EVAL-KEGG/${RUN_TAG}/${RESULT_NAME}}" +DATA="${DATA:-${PRISM_DATA_ROOT}/dna_local_shards_${SPLIT}/kegg}" + +if [ -z "${CKPT:-}" ]; then + echo "ERROR: set CKPT=/checkpoints/step_final" + exit 1 +fi + +cd "${PROJ_DIR}" + +export PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +module use /soft/modulefiles +module load frameworks/2026.1.0 +module load hdf5 +source "${PRISM_VENV}/bin/activate" + +export HF_HOME="${HF_HOME:-/flare/ModCon/${USER}/cache/model_weights}" +export HF_HUB_OFFLINE=1 +export TRANSFORMERS_OFFLINE=1 +export HF_DATASETS_OFFLINE=1 +export TMPDIR=/tmp +export PYTHONUNBUFFERED=1 +export PYTHONWARNINGS=ignore +export ZE_FLAT_DEVICE_HIERARCHY=FLAT +export ZE_AFFINITY_MASK=0 +# Text + 2 x max_dna_length (1024) DNA slots is ~2.8-3.6k tokens for KEGG; the +# default 2048 merged-length guard (model.py, issue #123) would reject it. +export MAX_SEQ_LENGTH="${MAX_SEQ_LENGTH:-4096}" + +for p in "${CKPT}/model.safetensors" "${DATA}/dataset_info.json" ${GRPO_ADAPTER:+"${GRPO_ADAPTER}"}; do + if [ ! -e "$p" ]; then + echo "ERROR: missing required path: $p" + exit 1 + fi +done + +echo "================================================================" +echo " BIOREASON KEGG EVAL (Aurora, 1 tile)" +echo " Checkpoint: ${CKPT}" +echo " GRPO adapter: ${GRPO_ADAPTER:-}" +echo " Data: ${DATA} (limit ${LIMIT:-all})" +echo " DNA mode: ${DNA_MODE}" +echo " Output: ${OUTPUT}" +echo "================================================================" + +python -u applications/bioreason/eval_kegg_aurora.py \ + --ckpt "${CKPT}" \ + ${GRPO_ADAPTER:+--grpo-adapter "${GRPO_ADAPTER}"} \ + --data "${DATA}" \ + --output "${OUTPUT}" \ + --max-new-tokens "${MAX_NEW_TOKENS}" \ + --dna-mode "${DNA_MODE}" \ + ${LIMIT:+--limit "${LIMIT}"} diff --git a/applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh b/applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh new file mode 100755 index 0000000..da38efd --- /dev/null +++ b/applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Submit the BioReason SFT validation sweep: one scripts/eval/submit_eval_kegg_aurora.sh job per config, +# run in parallel (login node; this script only calls qsub). +# +# real DNA on every checkpoint -> accuracy / F1 vs. training step (overfit check) +# shuffled DNA on step_final -> does the model actually use the DNA? +# blank DNA on step_final +# +# Usage: +# bash applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh /checkpoints +# SPLIT=test LIMIT=20 WALLTIME=00:30:00 bash applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh +# Select checkpoints on SPLIT=val (default); run SPLIT=test once on the chosen one. +# Then: python applications/bioreason/helpers/summarize_eval_sweep.py /BIOREASON-EVAL-KEGG/ + +set -eo pipefail +CKPT_DIR="${1:?usage: $0 /checkpoints}" +SPLIT="${SPLIT:-val}" +WALLTIME="${WALLTIME:-01:00:00}" +TAG="${TAG:-sweep-$(date +%Y%m%d-%H%M%S)}" +PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/${USER}/output}" +OUT_DIR="${PRISM_OUTPUT_ROOT}/BIOREASON-EVAL-KEGG/${TAG}" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +mkdir -p "${OUT_DIR}" +echo "Sweep tag: ${TAG} split: ${SPLIT} outputs: ${OUT_DIR}" + +submit() { # + local vars="CKPT=$1,SPLIT=${SPLIT},DNA_MODE=$2,OUTPUT=${OUT_DIR}/$3.json,RUN_TAG=${TAG}" + [ -n "${LIMIT:-}" ] && vars+=",LIMIT=${LIMIT}" + qsub -l walltime="${WALLTIME}" -v "${vars}" "${HERE}/submit_eval_kegg_aurora.sh" +} + +shopt -s nullglob +found=0 +for c in "${CKPT_DIR}"/step_[0-9]* ; do + [ -f "${c}/model.safetensors" ] || continue + submit "${c}" real "$(basename "${c}")_real"; found=1 +done +FINAL="${CKPT_DIR}/step_final" +if [ -f "${FINAL}/model.safetensors" ]; then + submit "${FINAL}" real step_final_real + submit "${FINAL}" shuffled step_final_shuffled + submit "${FINAL}" blank step_final_blank + found=1 +fi +[ "${found}" = 1 ] || { echo "ERROR: no checkpoints with model.safetensors under ${CKPT_DIR}"; exit 1; } +echo "Submitted. Summarize with: python applications/bioreason/helpers/summarize_eval_sweep.py ${OUT_DIR}" diff --git a/applications/bioreason/verify_dna_dataset_mix.sh b/applications/bioreason/scripts/test/verify_dna_dataset_mix.sh similarity index 96% rename from applications/bioreason/verify_dna_dataset_mix.sh rename to applications/bioreason/scripts/test/verify_dna_dataset_mix.sh index 92df0c9..5153924 100644 --- a/applications/bioreason/verify_dna_dataset_mix.sh +++ b/applications/bioreason/scripts/test/verify_dna_dataset_mix.sh @@ -18,8 +18,8 @@ # 3. Training doesn't crash on the new answer formats/handlers # # Usage: -# qsub applications/bioreason/verify_dna_dataset_mix.sh -# bash applications/bioreason/verify_dna_dataset_mix.sh # on an already-allocated compute node +# qsub applications/bioreason/scripts/test/verify_dna_dataset_mix.sh +# bash applications/bioreason/scripts/test/verify_dna_dataset_mix.sh # on an already-allocated compute node set -euo pipefail diff --git a/applications/bioreason/submit_bioreason_grpo.sh b/applications/bioreason/scripts/training/submit_bioreason_grpo.sh similarity index 100% rename from applications/bioreason/submit_bioreason_grpo.sh rename to applications/bioreason/scripts/training/submit_bioreason_grpo.sh diff --git a/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh b/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh new file mode 100755 index 0000000..fe9ae10 --- /dev/null +++ b/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh @@ -0,0 +1,141 @@ +#!/bin/bash -l +#PBS -N bioreason-grpo-kegg-1b +#PBS -l select=1 +#PBS -l walltime=04:00:00 +#PBS -l filesystems=home:flare +#PBS -q prod +#PBS -A ModCon +#PBS -k doe +#PBS -j oe +# +# Aurora XPU launcher: BioReason Stage 3 GRPO on KEGG (wanglab/kegg) only. +# Aurora port of submit_bioreason_grpo.sh (Polaris/CUDA). +# base model : OLMo-1B (model=prism_olmo_1b) -- training=bioreason_grpo alone +# would fall back to config.yaml's default prism_olmo3_7b +# start from : Stage 2 SFT checkpoint (model.safetensors + lora_adapter.pt); +# the SFT adapter (r=32, alpha=64) is merged into the base, then +# a fresh r=16 GRPO adapter trains on top +# frozen : NT-2.5B DNA encoder, DNA projector, base LLM +# saves : checkpoints/step_*/lora_adapter.pt only (no model.safetensors), +# so eval needs the SFT checkpoint + this adapter +# +# ZoneDTrainer is single-process by design (no DDP/vLLM), so this runs ONE rank +# on ONE tile; the other 11 tiles of the node sit idle. +# +# Batch job: +# qsub -v STAGE1_CKPT=/checkpoints/step_final applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh +# qsub -v STAGE1_CKPT=...,MAX_STEPS=50,RUN_TAG=demo applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh +# Interactive node: +# STAGE1_CKPT=... MAX_STEPS=5 bash applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh +# +# Outputs land in ${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG} (override with +# HYDRA_RUN_DIR); EXTRA_HYDRA is appended verbatim to the train.py command. + +set -eo pipefail + +PROJ_DIR="${PROJ_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." 2>/dev/null && pwd)}" +# Under qsub the script runs from a spool copy, so BASH_SOURCE points there. +if [ -n "${PBS_O_WORKDIR:-}" ] && [ ! -f "${PROJ_DIR}/src/train.py" ]; then + PROJ_DIR="${PBS_O_WORKDIR}" +fi +RUN_ID="${RUN_ID:-BIOREASON-GRPO-KEGG}" +RUN_VARIANT="1B" +RUN_TAG="${RUN_TAG:-$(date +%Y%m%d-%H%M%S)}" +MAX_STEPS="${MAX_STEPS:-1000}" +NUM_GENERATIONS="${NUM_GENERATIONS:-8}" +MAX_COMPLETION="${MAX_COMPLETION:-800}" +SAVE_EVERY="${SAVE_EVERY:-100}" +EXTRA_HYDRA="${EXTRA_HYDRA:-}" + +export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects/ModCon/${USER}/cache}" +export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/${USER}/output}" +export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects/ModCon/${USER}/modcon/checkpoints}" +HYDRA_RUN_DIR="${HYDRA_RUN_DIR:-${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG}}" + +# Stage 2 SFT checkpoint. Default: newest SFT step_final under PRISM_CKPT_ROOT. +STAGE1_CKPT="${STAGE1_CKPT:-$(ls -td "${PRISM_CKPT_ROOT}/BIOREASON-SFT-KEGG"/*/checkpoints/step_final 2>/dev/null | head -n 1)}" +if [ -z "${STAGE1_CKPT}" ]; then + echo "ERROR: no SFT checkpoint found -- run scripts/training/submit_bioreason_sft.sh or set STAGE1_CKPT." + exit 1 +fi + +cd "${PROJ_DIR}" + +export PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +module use /soft/modulefiles +module load frameworks/2026.1.0 +module load hdf5 +if [ ! -f "${PRISM_VENV}/bin/activate" ]; then + echo "ERROR: PRISM_VENV not found: ${PRISM_VENV} (bash applications/bioreason/helpers/setup_jupyter_kernel.sh builds it)" + exit 1 +fi +source "${PRISM_VENV}/bin/activate" + +if [[ ! "${HOSTNAME}" =~ aurora-uan ]]; then + export HTTP_PROXY="http://proxy.alcf.anl.gov:3128" + export HTTPS_PROXY="http://proxy.alcf.anl.gov:3128" + export http_proxy="http://proxy.alcf.anl.gov:3128" + export https_proxy="http://proxy.alcf.anl.gov:3128" +fi + +export HF_HOME="${HF_HOME:-/flare/ModCon/${USER}/cache/model_weights}" +export HF_HUB_OFFLINE=1 +export TRANSFORMERS_OFFLINE=1 +export HF_DATASETS_OFFLINE=1 +export WANDB_MODE="${WANDB_MODE:-offline}" +export WANDB_SILENT=true +export TMPDIR=/tmp +export PYTHONUNBUFFERED=1 +export PYTHONFAULTHANDLER=1 +export PYTHONWARNINGS=ignore +export NUMEXPR_MAX_THREADS=512 +export NUMEXPR_NUM_THREADS=128 +export ZE_FLAT_DEVICE_HIERARCHY=FLAT +export ZE_ENABLE_PCI_ID_DEVICE_ORDER=1 +# Text + 2 x max_dna_length (1024) DNA slots is ~2.8-3.6k tokens for KEGG; the +# default 2048 merged-length guard (model.py, issue #123) would reject it. +export MAX_SEQ_LENGTH="${MAX_SEQ_LENGTH:-4096}" + +for p in "${STAGE1_CKPT}/model.safetensors" "${STAGE1_CKPT}/lora_adapter.pt" \ + "${PRISM_DATA_ROOT}/dna_local_shards/kegg/dataset_info.json" \ + "${HF_HOME}/hub/models--allenai--OLMo-1B-0724-hf" \ + "${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species"; do + if [ ! -e "$p" ]; then + echo "ERROR: missing required path: $p" + exit 1 + fi +done + +echo "================================================================" +echo " BIOREASON GRPO (KEGG only, OLMo-1B + LoRA r16, Aurora, 1 tile)" +echo " Node: $(hostname)" +echo " SFT ckpt: ${STAGE1_CKPT}" +echo " Run dir: ${HYDRA_RUN_DIR}" +echo " Max steps: ${MAX_STEPS} (G=${NUM_GENERATIONS}, max completion ${MAX_COMPLETION})" +echo "================================================================" + +mpiexec -n 1 -ppn 1 --cpu-bind depth --depth 8 bash -lc ' + module use /soft/modulefiles + module load frameworks/2026.1.0 + source '"${PRISM_VENV}"'/bin/activate + export ZE_AFFINITY_MASK=0 + export WORLD_SIZE=1 RANK=0 LOCAL_RANK=0 LOCAL_WORLD_SIZE=1 + + cd '"${PROJ_DIR}"' + python -u src/train.py \ + training=bioreason_grpo \ + model=prism_olmo_1b \ + exp.id='"${RUN_ID}"' \ + exp.variant='"${RUN_VARIANT}"' \ + training.resume_weights_only='"${STAGE1_CKPT}"' \ + training.bioreason_dataset=wanglab/kegg \ + training.max_steps='"${MAX_STEPS}"' \ + training.grpo_num_generations='"${NUM_GENERATIONS}"' \ + training.grpo_max_completion_length='"${MAX_COMPLETION}"' \ + training.save_every_n_steps='"${SAVE_EVERY}"' \ + wandb.mode='"${WANDB_MODE}"' \ + hydra.run.dir='"${HYDRA_RUN_DIR}"' '"${EXTRA_HYDRA}"' +' + +echo "" +echo " GRPO adapter: ${HYDRA_RUN_DIR}/checkpoints/step_final/lora_adapter.pt" diff --git a/applications/bioreason/scripts/training/submit_bioreason_projector.sh b/applications/bioreason/scripts/training/submit_bioreason_projector.sh new file mode 100755 index 0000000..af13779 --- /dev/null +++ b/applications/bioreason/scripts/training/submit_bioreason_projector.sh @@ -0,0 +1,177 @@ +#!/bin/bash -l +#PBS -N bioreason-projector-kegg-1b +#PBS -l select=1 +#PBS -l walltime=01:00:00 +#PBS -l filesystems=home:flare +#PBS -q debug +#PBS -A ModCon +#PBS -k doe +#PBS -j oe +# +# Aurora XPU launcher: BioReason Stage 1 projector warmup on KEGG (wanglab/kegg) only. +# base model : OLMo-1B (model=prism_olmo_1b), backbone frozen +# trainable : DNA projector only (no LoRA) +# frozen : NT-2.5B DNA encoder +# Its checkpoints/step_final is the STAGE1_CKPT for scripts/training/submit_bioreason_sft.sh. +# Outputs land in ${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG} (override with HYDRA_RUN_DIR). +# +# Defaults: 100 optimizer steps, grad-accum 1, warmup 10. On 12 tiles x batch 8 +# that is 96 samples/step -> 9,600 samples (~8 epochs of KEGG's 1,159 train rows). +# bioreason_projector.yaml's own grad-accum of 8 would make 100 steps ~66 epochs. +# +# Batch job: +# qsub applications/bioreason/scripts/training/submit_bioreason_projector.sh +# qsub -v MAX_STEPS=200,GRAD_ACCUM=2 applications/bioreason/scripts/training/submit_bioreason_projector.sh +# +# Interactive node (after `qsub -I -l select=1,walltime=01:00:00,filesystems=home:flare -q debug -A ModCon`): +# bash applications/bioreason/scripts/training/submit_bioreason_projector.sh + +set -eo pipefail + +PROJ_DIR="/home/abalaji/projects/modcon/release/BaseMM_PRISM" +RUN_ID="${RUN_ID:-BIOREASON-PROJECTOR-KEGG}" +RUN_VARIANT="1B" +RUN_TAG="${RUN_TAG:-$(date +%Y%m%d-%H%M%S)}" +MAX_STEPS="${MAX_STEPS:-100}" +WARMUP_STEPS="${WARMUP_STEPS:-10}" +GRAD_ACCUM="${GRAD_ACCUM:-1}" +BATCH_SIZE="${BATCH_SIZE:-1}" # batch 8 at ~3k merged tokens OOMs a tile (see SFT launcher) +SAVE_EVERY="${SAVE_EVERY:-50}" +NPERNODE="${NPERNODE:-12}" # 6 PVC GPUs x 2 tiles per Aurora node (FLAT mode) + +# Site roots: datasets_config.json resolves dna_bioreason's local_path as +# ${PRISM_DATA_ROOT}/dna_local_shards/kegg. +export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects/ModCon/abalaji/cache}" +export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/abalaji/output}" +# Checkpoints (~13 GB each: frozen backbone + encoder are saved too) go to flare, +# not Hydra's default outputs/ under the repo in /home, which overflows its quota. +export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects/ModCon/abalaji/modcon/checkpoints}" +HYDRA_RUN_DIR="${HYDRA_RUN_DIR:-${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG}}" + +cd "${PROJ_DIR}" + +# frameworks module + a --system-site-packages venv adding only what the module +# lacks (wandb, torchtune, webdataset, braceexpand). Not the repo's .venv, +# which carries a CUDA torch build. Build it once (from a UAN) with: +# module load frameworks/2026.1.0 +# uv venv --system-site-packages -p "$(which python)" $PRISM_VENV && source $PRISM_VENV/bin/activate +# uv pip install wandb braceexpand==0.1.7 && uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 +export PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +module use /soft/modulefiles +module load frameworks/2026.1.0 +module load hdf5 +if [ ! -f "${PRISM_VENV}/bin/activate" ]; then + echo "ERROR: PRISM_VENV not found: ${PRISM_VENV} (see build steps above)" + exit 1 +fi +source "${PRISM_VENV}/bin/activate" +source "${PROJ_DIR}/.venv/bin/activate" + +if [[ ! "${HOSTNAME}" =~ aurora-uan ]]; then + export HTTP_PROXY="http://proxy.alcf.anl.gov:3128" + export HTTPS_PROXY="http://proxy.alcf.anl.gov:3128" + export http_proxy="http://proxy.alcf.anl.gov:3128" + export https_proxy="http://proxy.alcf.anl.gov:3128" +fi + +# OLMo-1B-0724-hf, nucleotide-transformer-2.5b-multi-species and +# datasets--wanglab--kegg are all pre-cached here; run fully offline. +export HF_HOME="/flare/ModCon/${USER}/cache/model_weights" +export HF_HUB_OFFLINE=1 +export TRANSFORMERS_OFFLINE=1 +export HF_DATASETS_OFFLINE=1 +# Text + 2 x 1024 DNA slots is ~2.8-3.6k merged tokens; default guard is 2048. +export MAX_SEQ_LENGTH="${MAX_SEQ_LENGTH:-4096}" +export WANDB_MODE="${WANDB_MODE:-offline}" +export WANDB_SILENT=true +# PBS's default TMPDIR overflows the 108-byte AF_UNIX socket path limit that +# torch's DataLoader worker IPC relies on -- force a short one. +export TMPDIR=/tmp +# DDP side-copy streams index torch.accelerator.device_count(), which is 0 +# under ZE_AFFINITY_MASK on Aurora -> IndexError on first forward. +export PYTORCH_DDP_USE_SIDE_STREAM=0 +export PYTHONUNBUFFERED=1 +export PYTHONFAULTHANDLER=1 +export PYTHONWARNINGS=ignore +export NUMEXPR_MAX_THREADS=512 +export NUMEXPR_NUM_THREADS=128 +export ZE_FLAT_DEVICE_HIERARCHY=FLAT +export ZE_ENABLE_PCI_ID_DEVICE_ORDER=1 + +# Falls back to the current host on an interactive node without PBS_NODEFILE. +if [ -n "${PBS_NODEFILE:-}" ]; then + MASTER_ADDR=$(head -n 1 "$PBS_NODEFILE") +else + MASTER_ADDR=$(hostname) +fi +export MASTER_ADDR +export MASTER_PORT=$((20000 + RANDOM % 20000)) + +for p in "${PRISM_DATA_ROOT}/dna_local_shards/kegg/dataset_info.json" \ + "${HF_HOME}/hub/models--allenai--OLMo-1B-0724-hf" \ + "${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species"; do + if [ ! -e "$p" ]; then + echo "ERROR: missing required path: $p" + exit 1 + fi +done + +echo "================================================================" +echo " BIOREASON STAGE 1 PROJECTOR WARMUP (KEGG only, OLMo-1B, Aurora)" +echo " Node: $(hostname)" +echo " Master: ${MASTER_ADDR}:${MASTER_PORT}" +echo " Ranks: ${NPERNODE}" +echo " KEGG shard: ${PRISM_DATA_ROOT}/dna_local_shards/kegg" +echo " Steps: ${MAX_STEPS} (batch ${BATCH_SIZE}, warmup ${WARMUP_STEPS}, grad-accum ${GRAD_ACCUM}, save every ${SAVE_EVERY})" +echo " Run dir: ${HYDRA_RUN_DIR}" +echo "================================================================" + +mpiexec -n "${NPERNODE}" -ppn "${NPERNODE}" --cpu-bind depth --depth 8 bash -lc ' + module use /soft/modulefiles + module load frameworks/2026.1.0 + source '"${PRISM_VENV}"'/bin/activate + + export CCL_PROCESS_LAUNCHER=none + export CCL_ATL_TRANSPORT=ofi + export CCL_OP_SYNC=1 + export CCL_WORKER_COUNT=1 + export CCL_ALLREDUCE=ring + export CCL_OFI_ENABLE_HOSTNAME_SHARING=0 + export FI_CXI_RX_MATCH_MODE=hybrid + + export WORLD_SIZE=${PMI_SIZE:-${PALS_SIZE:-'"${NPERNODE}"'}} + export RANK=${PMI_RANK:-${PALS_RANKID:-0}} + export LOCAL_RANK=${PMI_LOCAL_RANK:-${PALS_LOCAL_RANKID:-0}} + export LOCAL_WORLD_SIZE=${PMI_LOCAL_SIZE:-${PALS_LOCAL_SIZE:-'"${NPERNODE}"'}} + export ZE_AFFINITY_MASK=$LOCAL_RANK + # Each rank now sees exactly one tile, as xpu:0. Accelerate/DDP build the + # device from LOCAL_RANK, so leaving it at 0..11 targets xpu: -> + # "device index is out of range". Same normalization as tools/launch_aurora_web.py. + export ACCELERATE_ORIG_LOCAL_RANK=$LOCAL_RANK + export LOCAL_RANK=0 PMI_LOCAL_RANK=0 PMIX_LOCAL_RANK=0 PALS_LOCAL_RANKID=0 OMPI_COMM_WORLD_LOCAL_RANK=0 + export MASTER_ADDR='"${MASTER_ADDR}"' + export MASTER_PORT='"${MASTER_PORT}"' + + cd '"${PROJ_DIR}"' + python -u src/train.py \ + training=bioreason_projector \ + model=prism_olmo_1b \ + exp.id='"${RUN_ID}"' \ + exp.variant='"${RUN_VARIANT}"' \ + training.bioreason_dataset=wanglab/kegg \ + training.batch_size='"${BATCH_SIZE}"' \ + training.max_steps='"${MAX_STEPS}"' \ + training.warmup_steps='"${WARMUP_STEPS}"' \ + training.gradient_accumulation_steps='"${GRAD_ACCUM}"' \ + training.save_every_n_steps='"${SAVE_EVERY}"' \ + wandb.mode='"${WANDB_MODE}"' \ + hydra.run.dir='"${HYDRA_RUN_DIR}"' +' + +CKPT="${HYDRA_RUN_DIR}/checkpoints/step_final" +[ -f "${CKPT}/model.safetensors" ] || CKPT="" +echo "" +echo "================================================================" +echo " STAGE 1 DONE. Projector checkpoint: ${CKPT:-}" +echo " Stage 2: qsub -v STAGE1_CKPT=${CKPT} applications/bioreason/scripts/training/submit_bioreason_sft.sh" +echo "================================================================" diff --git a/applications/bioreason/scripts/training/submit_bioreason_sft.sh b/applications/bioreason/scripts/training/submit_bioreason_sft.sh new file mode 100644 index 0000000..c570fbf --- /dev/null +++ b/applications/bioreason/scripts/training/submit_bioreason_sft.sh @@ -0,0 +1,174 @@ +#!/bin/bash -l +#PBS -N bioreason-sft-kegg-1b +#PBS -l select=1 +#PBS -l walltime=06:00:00 +#PBS -l filesystems=home:flare +#PBS -q capacity +#PBS -A ModCon +#PBS -k doe +#PBS -j oe +# +# Aurora XPU launcher: BioReason Stage 2 SFT on KEGG (wanglab/kegg) only. +# base model : OLMo-1B (model=prism_olmo_1b), backbone frozen +# trainable : DNA projector (random init, no Stage 1) + LoRA adapters +# frozen : NT-2.5B DNA encoder +# loss : assistant span only +# (\n{reasoning}\n\n\nAnswer: {answer}<|im_end|>) +# +# Batch job: +# qsub applications/bioreason/scripts/training/submit_bioreason_sft.sh +# qsub -v MAX_STEPS=2000 applications/bioreason/scripts/training/submit_bioreason_sft.sh +# # optional: start from a pretrained projector instead of random init +# qsub -v STAGE1_CKPT=/path/to/checkpoints/step_N applications/bioreason/scripts/training/submit_bioreason_sft.sh +# +# Interactive node (after `qsub -I -l select=1,walltime=01:00:00,filesystems=home:flare -q debug -A ModCon`): +# bash applications/bioreason/scripts/training/submit_bioreason_sft.sh +# Single-tile debug run: +# NPERNODE=1 MAX_STEPS=20 bash applications/bioreason/scripts/training/submit_bioreason_sft.sh + +set -eo pipefail + +PROJ_DIR="/home/abalaji/projects/modcon/release/BaseMM_PRISM" +RUN_ID="${RUN_ID:-BIOREASON-SFT-KEGG}" +RUN_VARIANT="1B" +RUN_TAG="${RUN_TAG:-$(date +%Y%m%d-%H%M%S)}" +MAX_STEPS="${MAX_STEPS:-500}" +NPERNODE="${NPERNODE:-12}" # 6 PVC GPUs x 2 tiles per Aurora node (FLAT mode) + +# Site roots: datasets_config.json resolves dna_bioreason's local_path as +# ${PRISM_DATA_ROOT}/dna_local_shards/kegg. +export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects/ModCon/abalaji/cache}" +export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/abalaji/output}" +# Checkpoints (~13 GB each) go to flare, not Hydra's default outputs/ under the +# repo in /home, which overflows its quota. +export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects/ModCon/abalaji/modcon/checkpoints}" +HYDRA_RUN_DIR="${HYDRA_RUN_DIR:-${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG}}" + +# Optional: Stage 1 (bioreason_projector, model=prism_olmo_1b) checkpoint. Unset +# (default) = projector trains from random init, as in BioReason. If set, its +# output width must match OLMo-1B's d_model (2048). +STAGE1_CKPT="${STAGE1_CKPT:-}" +RESUME_ARG="" +if [ -n "${STAGE1_CKPT}" ]; then + RESUME_ARG="training.resume_weights_only=${STAGE1_CKPT}" +fi + +cd "${PROJ_DIR}" + +# frameworks module + a --system-site-packages venv adding only what the module +# lacks (wandb, torchtune, webdataset, braceexpand). Not the repo's .venv, +# which carries a CUDA torch build. Build it once (from a UAN) with: +# module load frameworks/2026.1.0 +# uv venv --system-site-packages -p "$(which python)" $PRISM_VENV && source $PRISM_VENV/bin/activate +# uv pip install wandb braceexpand==0.1.7 && uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 +export PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +module use /soft/modulefiles +module load frameworks/2026.1.0 +module load hdf5 +if [ ! -f "${PRISM_VENV}/bin/activate" ]; then + echo "ERROR: PRISM_VENV not found: ${PRISM_VENV} (see build steps above)" + exit 1 +fi +source "${PRISM_VENV}/bin/activate" + +if [[ ! "${HOSTNAME}" =~ aurora-uan ]]; then + export HTTP_PROXY="http://proxy.alcf.anl.gov:3128" + export HTTPS_PROXY="http://proxy.alcf.anl.gov:3128" + export http_proxy="http://proxy.alcf.anl.gov:3128" + export https_proxy="http://proxy.alcf.anl.gov:3128" +fi + +# OLMo-1B-0724-hf, nucleotide-transformer-2.5b-multi-species and +# datasets--wanglab--kegg are all pre-cached here; run fully offline. +export HF_HOME="/flare/ModCon/${USER}/cache/model_weights" +export HF_HUB_OFFLINE=1 +export TRANSFORMERS_OFFLINE=1 +export HF_DATASETS_OFFLINE=1 +export WANDB_MODE="${WANDB_MODE:-offline}" +export WANDB_SILENT=true +# PBS's default TMPDIR overflows the 108-byte AF_UNIX socket path limit that +# torch's DataLoader worker IPC relies on -- force a short one. +export TMPDIR=/tmp +# DDP side-copy streams index torch.accelerator.device_count(), which is 0 +# under ZE_AFFINITY_MASK on Aurora -> IndexError on first forward. +export PYTORCH_DDP_USE_SIDE_STREAM=0 +export PYTHONUNBUFFERED=1 +export PYTHONFAULTHANDLER=1 +export PYTHONWARNINGS=ignore +export NUMEXPR_MAX_THREADS=512 +export NUMEXPR_NUM_THREADS=128 +export ZE_FLAT_DEVICE_HIERARCHY=FLAT +export ZE_ENABLE_PCI_ID_DEVICE_ORDER=1 +# Text + 2 x max_dna_length (1024) DNA slots is ~2.8-3.6k tokens for KEGG; the +# default 2048 merged-length guard (model.py, issue #123) would reject it. +export MAX_SEQ_LENGTH="${MAX_SEQ_LENGTH:-4096}" + +# Falls back to the current host on an interactive node without PBS_NODEFILE. +if [ -n "${PBS_NODEFILE:-}" ]; then + MASTER_ADDR=$(head -n 1 "$PBS_NODEFILE") +else + MASTER_ADDR=$(hostname) +fi +export MASTER_ADDR +export MASTER_PORT=$((20000 + RANDOM % 20000)) + +REQUIRED=("${PRISM_DATA_ROOT}/dna_local_shards/kegg/dataset_info.json" \ + "${HF_HOME}/hub/models--allenai--OLMo-1B-0724-hf" \ + "${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species") +[ -n "${STAGE1_CKPT}" ] && REQUIRED+=("${STAGE1_CKPT}/model.safetensors") +for p in "${REQUIRED[@]}"; do + if [ ! -e "$p" ]; then + echo "ERROR: missing required path: $p" + exit 1 + fi +done + +echo "================================================================" +echo " BIOREASON SFT (KEGG only, OLMo-1B + LoRA + projector, Aurora)" +echo " Node: $(hostname)" +echo " Master: ${MASTER_ADDR}:${MASTER_PORT}" +echo " Ranks: ${NPERNODE}" +echo " Projector init: ${STAGE1_CKPT:-random (no Stage 1)}" +echo " KEGG shard: ${PRISM_DATA_ROOT}/dna_local_shards/kegg" +echo " Max steps: ${MAX_STEPS}" +echo " Run dir: ${HYDRA_RUN_DIR}" +echo "================================================================" + +mpiexec -n "${NPERNODE}" -ppn "${NPERNODE}" --cpu-bind depth --depth 8 bash -lc ' + module use /soft/modulefiles + module load frameworks/2026.1.0 + source '"${PRISM_VENV}"'/bin/activate + + export CCL_PROCESS_LAUNCHER=none + export CCL_ATL_TRANSPORT=ofi + export CCL_OP_SYNC=1 + export CCL_WORKER_COUNT=1 + export CCL_ALLREDUCE=ring + export CCL_OFI_ENABLE_HOSTNAME_SHARING=0 + export FI_CXI_RX_MATCH_MODE=hybrid + + export WORLD_SIZE=${PMI_SIZE:-${PALS_SIZE:-'"${NPERNODE}"'}} + export RANK=${PMI_RANK:-${PALS_RANKID:-0}} + export LOCAL_RANK=${PMI_LOCAL_RANK:-${PALS_LOCAL_RANKID:-0}} + export LOCAL_WORLD_SIZE=${PMI_LOCAL_SIZE:-${PALS_LOCAL_SIZE:-'"${NPERNODE}"'}} + export ZE_AFFINITY_MASK=$LOCAL_RANK + # Each rank now sees exactly one tile, as xpu:0. Accelerate/DDP build the + # device from LOCAL_RANK, so leaving it at 0..11 targets xpu: -> + # "device index is out of range". Same normalization as tools/launch_aurora_web.py. + export ACCELERATE_ORIG_LOCAL_RANK=$LOCAL_RANK + export LOCAL_RANK=0 PMI_LOCAL_RANK=0 PMIX_LOCAL_RANK=0 PALS_LOCAL_RANKID=0 OMPI_COMM_WORLD_LOCAL_RANK=0 + export MASTER_ADDR='"${MASTER_ADDR}"' + export MASTER_PORT='"${MASTER_PORT}"' + + cd '"${PROJ_DIR}"' + python -u src/train.py \ + training=bioreason_sft \ + model=prism_olmo_1b \ + exp.id='"${RUN_ID}"' \ + exp.variant='"${RUN_VARIANT}"' \ + '"${RESUME_ARG}"' \ + training.bioreason_dataset=wanglab/kegg \ + training.max_steps='"${MAX_STEPS}"' \ + wandb.mode='"${WANDB_MODE}"' \ + hydra.run.dir='"${HYDRA_RUN_DIR}"' +' diff --git a/applications/bioreason/submit_bioreason_projector.sh b/applications/bioreason/submit_bioreason_projector.sh deleted file mode 100755 index 49ad8e5..0000000 --- a/applications/bioreason/submit_bioreason_projector.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash -# Polaris-specific launcher (PBS/mpiexec/cudatoolkit-standalone/argonne_tpc -# allocation) -- unlike this repo's other, Aurora-first launchers. -#PBS -N bioreason_projector_kegg -#PBS -l select=1 -#PBS -l walltime=04:00:00 -#PBS -j oe -#PBS -l filesystems=home:eagle -#PBS -A argonne_tpc -#PBS -q preemptable - -set -euo pipefail - -PROJ_DIR="/home/abalaji/projects/modcon/genome/BaseMM_PRISM" -OUTPUT_BASE="/lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output" -RUN_ID="GENOME-PROJECTOR-KEGG" -RUN_VARIANT="1B" - -# Set to a checkpoint directory to resume this same stage (optimizer/scheduler/step -# included), e.g. after preemption. Leave empty to start a fresh run. -# RESUME_CKPT="${OUTPUT_BASE}/GENOME-PROJECTOR-1B/2026-07-01/12-00-00/checkpoints/step_2500" -RESUME_CKPT="" - -cd "${PROJ_DIR}" - -# --- Cuda and NCCL (Polaris) --- -ml use /soft/modulefiles -ml --ignore_cache load cudatoolkit-standalone/12.6.1 - -# --- Conda env --- -ml --ignore_cache load conda -conda activate /lus/eagle/projects/argonne_tpc/abalaji/conda_env/prism - -# --- Proxy --- -export HTTP_PROXY="http://proxy.alcf.anl.gov:3128" -export HTTPS_PROXY="http://proxy.alcf.anl.gov:3128" -export http_proxy="http://proxy.alcf.anl.gov:3128" -export https_proxy="http://proxy.alcf.anl.gov:3128" -export no_proxy="127.0.0.1,admin,polaris-adminvm-01,localhost,*.cm.polaris.alcf.anl.gov,polaris-*,*.polaris.alcf.anl.gov,*.alcf.anl.gov" - -# --- HuggingFace cache --- -export HF_HOME="/lus/eagle/projects/argonne_tpc/abalaji/model_weights" -export HF_DATASETS_CACHE="/lus/eagle/projects/argonne_tpc/abalaji/model_weights" -export TRANSFORMERS_CACHE="/lus/eagle/projects/argonne_tpc/abalaji/model_weights/hub" -export HF_HUB_CACHE="/lus/eagle/projects/argonne_tpc/abalaji/model_weights/hub" - -# --- W&B --- -# No WANDB_API_KEY here -- a plaintext key was committed here previously -# and has been removed; rotate that key on wandb.ai if it hasn't been -# already. wandb picks up credentials from ~/.netrc (machine api.wandb.ai) -# or an already-exported WANDB_API_KEY in the calling shell automatically -# -- nothing to set here. - -# --- NCCL (Polaris) --- -export NCCL_NET_GDR_LEVEL=PHB -export NCCL_CROSS_NIC=1 -export NCCL_COLLNET_ENABLE=1 -export LD_LIBRARY_PATH=/soft/libraries/aws-ofi-nccl/v1.9.1-aws/lib:${LD_LIBRARY_PATH:-} - -# --- General --- -export PYTHONUNBUFFERED=1 -export PYTHONFAULTHANDLER=1 -export PYTHONWARNINGS=ignore -export OMP_NUM_THREADS=8 -export NUMEXPR_MAX_THREADS=64 - -# --- Distributed --- -# Falls back to the current host when run directly on an already-allocated -# interactive node (no PBS_NODEFILE, e.g. after `qsub -I`). -if [ -n "${PBS_NODEFILE:-}" ]; then - NNODES=$(sort -u "$PBS_NODEFILE" | wc -l) - MASTER_ADDR=$(sort -u "$PBS_NODEFILE" | head -n 1) -else - NNODES=1 - MASTER_ADDR=$(hostname) -fi -NPERNODE=4 # 4 GPUs per Polaris node -NPROCS=$((NNODES * NPERNODE)) - -export MASTER_ADDR -export MASTER_PORT=$((20000 + RANDOM % 20000)) -export WORLD_SIZE=${NPROCS} - -echo "=== BioReason Projector Training (Stage 1) ===" -echo "Node: $(hostname)" -echo "PBS_NODEFILE: ${PBS_NODEFILE:-}" -echo "NNODES=${NNODES} NPERNODE=${NPERNODE} NPROCS=${NPROCS}" -echo "MASTER_ADDR=${MASTER_ADDR} MASTER_PORT=${MASTER_PORT}" -echo "Output base: ${OUTPUT_BASE}" -echo "Resume ckpt: ${RESUME_CKPT:-}" -echo "" - -# Unquoted splice below: when empty, this contributes zero words to the command -# line (not an empty-string arg), so train.py/Hydra sees a normal 3-override -# invocation when not resuming. -RESUME_OVERRIDE="" -if [ -n "${RESUME_CKPT}" ]; then - RESUME_OVERRIDE="training.resume_from_checkpoint=${RESUME_CKPT}" -fi - -mpiexec -n "${NPROCS}" -ppn "${NPERNODE}" \ - --envall \ - bash -lc ' - export RANK=${PMI_RANK:-${PALS_RANKID:-0}} - export WORLD_SIZE=${PMI_SIZE:-${PALS_SIZE:-'"${NPROCS}"'}} - export LOCAL_RANK=${PMI_LOCAL_RANK:-${PALS_LOCAL_RANKID:-0}} - export LOCAL_WORLD_SIZE=${PMI_LOCAL_SIZE:-${PALS_LOCAL_SIZE:-'"${NPERNODE}"'}} - export MASTER_ADDR="'"${MASTER_ADDR}"'" - export MASTER_PORT="'"${MASTER_PORT}"'" - - cd '"${PROJ_DIR}"' - python -u src/train.py \ - training=bioreason_projector \ - exp.id='"${RUN_ID}"' \ - exp.variant='"${RUN_VARIANT}"' \ - '"${RESUME_OVERRIDE}"' - ' diff --git a/applications/bioreason/submit_bioreason_sft.sh b/applications/bioreason/submit_bioreason_sft.sh deleted file mode 100644 index e69e99e..0000000 --- a/applications/bioreason/submit_bioreason_sft.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -# Polaris-specific launcher (PBS/mpiexec/cudatoolkit-standalone/argonne_tpc -# allocation) -- unlike this repo's other, Aurora-first launchers. -#PBS -N prism-sft -#PBS -l select=1 -#PBS -l walltime=07:00:00 -#PBS -j oe -#PBS -l filesystems=home:eagle -#PBS -A argonne_tpc -#PBS -q preemptable - -set -euo pipefail - -PROJ_DIR="/home/abalaji/projects/modcon/genome/BaseMM_PRISM" -OUTPUT_BASE="/lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output" -RUN_ID="GENOME-SFT" -RUN_VARIANT="1B" -STAGE1_CKPT="/lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output/GENOME-PROJECTOR-1B/2026-07-02/17-50-47/checkpoints/step_1000" -#/lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output/GENOME-SFT-1B/2026-06-28/18-18-57/checkpoints/step_3900 -#"/lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output/GENOME-PROJECTOR-1B/2026-06-18/16-57-40/checkpoints/step_1500" -cd "${PROJ_DIR}" - -# --- Cuda and NCCL (Polaris) --- -ml use /soft/modulefiles -ml --ignore_cache load cudatoolkit-standalone/12.6.1 - -# --- Conda env --- -ml --ignore_cache load conda -conda activate /lus/eagle/projects/argonne_tpc/abalaji/conda_env/prism - -# --- Proxy --- -export HTTP_PROXY="http://proxy.alcf.anl.gov:3128" -export HTTPS_PROXY="http://proxy.alcf.anl.gov:3128" -export http_proxy="http://proxy.alcf.anl.gov:3128" -export https_proxy="http://proxy.alcf.anl.gov:3128" -export no_proxy="127.0.0.1,admin,polaris-adminvm-01,localhost,*.cm.polaris.alcf.anl.gov,polaris-*,*.polaris.alcf.anl.gov,*.alcf.anl.gov" - -# --- HuggingFace cache --- -export HF_HOME="/lus/eagle/projects/argonne_tpc/abalaji/model_weights" -export HF_DATASETS_CACHE="/lus/eagle/projects/argonne_tpc/abalaji/model_weights" -export TRANSFORMERS_CACHE="/lus/eagle/projects/argonne_tpc/abalaji/model_weights/hub" -export HF_HUB_CACHE="/lus/eagle/projects/argonne_tpc/abalaji/model_weights/hub" - -# --- W&B --- -# No WANDB_API_KEY here -- a plaintext key was committed here previously -# and has been removed; rotate that key on wandb.ai if it hasn't been -# already. wandb picks up credentials from ~/.netrc (machine api.wandb.ai) -# or an already-exported WANDB_API_KEY in the calling shell automatically -# -- nothing to set here. - -# --- NCCL (Polaris) --- -export NCCL_NET_GDR_LEVEL=PHB -export NCCL_CROSS_NIC=1 -export NCCL_COLLNET_ENABLE=1 -export LD_LIBRARY_PATH=/soft/libraries/aws-ofi-nccl/v1.9.1-aws/lib:${LD_LIBRARY_PATH:-} - -# --- General --- -export PYTHONUNBUFFERED=1 -export PYTHONFAULTHANDLER=1 -export PYTHONWARNINGS=ignore -export OMP_NUM_THREADS=8 -export NUMEXPR_MAX_THREADS=64 - -# --- Distributed --- -# Falls back to the current host when run directly on an already-allocated -# interactive node (no PBS_NODEFILE, e.g. after `qsub -I`). -if [ -n "${PBS_NODEFILE:-}" ]; then - NNODES=$(sort -u "$PBS_NODEFILE" | wc -l) - MASTER_ADDR=$(sort -u "$PBS_NODEFILE" | head -n 1) -else - NNODES=1 - MASTER_ADDR=$(hostname) -fi -NPERNODE=4 # 4 GPUs per Polaris node -NPROCS=$((NNODES * NPERNODE)) - -export MASTER_ADDR -export MASTER_PORT=$((20000 + RANDOM % 20000)) -export WORLD_SIZE=${NPROCS} - -echo "=== BioReason SFT Training (Zone C / Stage 2) ===" -echo "Node: $(hostname)" -echo "PBS_NODEFILE: ${PBS_NODEFILE:-}" -echo "NNODES=${NNODES} NPERNODE=${NPERNODE} NPROCS=${NPROCS}" -echo "MASTER_ADDR=${MASTER_ADDR} MASTER_PORT=${MASTER_PORT}" -echo "Stage 1 ckpt: ${STAGE1_CKPT}" -echo "Output base: ${OUTPUT_BASE}" -echo "" - -mpiexec -n "${NPROCS}" -ppn "${NPERNODE}" \ - --envall \ - bash -lc ' - export RANK=${PMI_RANK:-${PALS_RANKID:-0}} - export WORLD_SIZE=${PMI_SIZE:-${PALS_SIZE:-'"${NPROCS}"'}} - export LOCAL_RANK=${PMI_LOCAL_RANK:-${PALS_LOCAL_RANKID:-0}} - export LOCAL_WORLD_SIZE=${PMI_LOCAL_SIZE:-${PALS_LOCAL_SIZE:-'"${NPERNODE}"'}} - export MASTER_ADDR="'"${MASTER_ADDR}"'" - export MASTER_PORT="'"${MASTER_PORT}"'" - - cd '"${PROJ_DIR}"' - python -u src/train.py \ - training=bioreason_sft \ - exp.id='"${RUN_ID}"' \ - exp.variant='"${RUN_VARIANT}"' \ - training.resume_weights_only='"${STAGE1_CKPT}"' - ' diff --git a/docs/applications/bioreason-eval.md b/docs/applications/bioreason-eval.md index ccec64b..9037870 100644 --- a/docs/applications/bioreason-eval.md +++ b/docs/applications/bioreason-eval.md @@ -4,7 +4,7 @@ Evaluates a BioReason SFT or GRPO checkpoint against the KEGG test split. > [!IMPORTANT] > `eval_kegg.py` is **not yet on `main`** — it ships with the eval-pipeline PR -> (#133). `applications/bioreason/submit_eval_kegg.sh` already invokes it, so both this page +> (#133). `applications/bioreason/scripts/eval/submit_eval_kegg.sh` already invokes it, so both this page > and that script only work once #133 lands. The invocation below is recorded > so the arguments are documented; it will not run against a clean checkout > today. diff --git a/docs/applications/bioreason_grpo.md b/docs/applications/bioreason_grpo.md index ecb127d..b20f520 100644 --- a/docs/applications/bioreason_grpo.md +++ b/docs/applications/bioreason_grpo.md @@ -29,9 +29,9 @@ the Stage 3 GRPO reinforcement learning implementation. Each stage loads weights from the previous via `training.resume_weights_only`. Submit scripts: -- Stage 1: `applications/bioreason/submit_bioreason_projector.sh` -- Stage 2: `applications/bioreason/submit_bioreason_sft.sh` -- Stage 3: `applications/bioreason/submit_bioreason_grpo.sh` +- Stage 1: `applications/bioreason/scripts/training/submit_bioreason_projector.sh` +- Stage 2: `applications/bioreason/scripts/training/submit_bioreason_sft.sh` +- Stage 3: `applications/bioreason/scripts/training/submit_bioreason_grpo.sh` --- diff --git a/src/conf/data/bioreason_demo_mix.yaml b/src/conf/data/bioreason_demo_mix.yaml new file mode 100644 index 0000000..0de6412 --- /dev/null +++ b/src/conf/data/bioreason_demo_mix.yaml @@ -0,0 +1,19 @@ +# Written by applications/bioreason/bioreason_demo.ipynb (section 5). +# KEGG + variant-effect mix (BioReason weights). Use with: data=bioreason_demo_mix +dataset_overrides: + dna_bioreason: + skip: false + preferred_source: local + weight: 0.5 + dna_bioreason_variant_effect_coding: + skip: false + preferred_source: local + weight: 0.25 + local_path: ${oc.env:PRISM_DATA_ROOT}/dna_local_shards/variant_effect_coding + dna_bioreason_variant_effect_non_snv: + skip: false + preferred_source: local + weight: 0.25 + local_path: ${oc.env:PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv + ts_qa: + skip: true diff --git a/src/conf/data/bioreason_kegg_only.yaml b/src/conf/data/bioreason_kegg_only.yaml new file mode 100644 index 0000000..8b74eec --- /dev/null +++ b/src/conf/data/bioreason_kegg_only.yaml @@ -0,0 +1,16 @@ +# BioReason KEGG-only data mix (wanglab/kegg via the dna_bioreason handler). +# Pins every zone_a dataset that is default-on in datasets_config.json off, so +# the SFT run cannot silently mix in another stream (ts_qa is skip=false there). +# +# Composed automatically by training=bioreason_{projector,sft,grpo} via their +# defaults list (/data@_global_.data) -- do not also pass +data=bioreason_kegg_only. +dataset_overrides: + dna_bioreason: + skip: false + preferred_source: local + dna_bioreason_variant_effect_coding: + skip: true + dna_bioreason_variant_effect_non_snv: + skip: true + ts_qa: + skip: true diff --git a/src/conf/model/prism_olmo_1b.yaml b/src/conf/model/prism_olmo_1b.yaml index 9a3bfcc..1e67355 100644 --- a/src/conf/model/prism_olmo_1b.yaml +++ b/src/conf/model/prism_olmo_1b.yaml @@ -51,6 +51,22 @@ modalities: # - "geometry" # - "graph" +# Interleaved DNA splice tokens. Authored as literal special-token strings +# (unlike time_series's pre-baked fixed integer ids) — UnifiedTransformer.__init__ +# registers any missing ones as new special tokens and resolves this to integer +# ids before the merge path runs. Must match _render_dna_prompt_text +# (src/data/multimodal.py) exactly. +# +# Required for correctness, not just for GRPO: without is_interleaved_qa=true, +# multimodal.py's DNA branch falls through to the "[dna_bioreason]" full- +# sequence-loss sentinel (the same one the projector-alignment stage uses) +# instead of computing the "P T" prompt/target split SFT needs to mask loss to +# just the assistant span. ZoneDTrainer (GRPO) also asserts this unconditionally. +is_interleaved_qa: true +modality_start_end_token_indices: + dna_reference: ["", ""] + dna_variant: ["", ""] + # IsoFLOP projector capacity knobs (defaults match ModelConfig dataclass) -- # Hydra struct mode rejects overrides for keys not declared in the YAML, # so callers can pass `model.projector_hidden_mult=N` without `+` prefix. diff --git a/src/conf/training/bioreason_grpo.yaml b/src/conf/training/bioreason_grpo.yaml index 35e4178..540f3ce 100644 --- a/src/conf/training/bioreason_grpo.yaml +++ b/src/conf/training/bioreason_grpo.yaml @@ -2,6 +2,12 @@ # Based on: https://arxiv.org/abs/2505.23579 # Stage 2: Group Relative Policy Optimization for biological reasoning +# KEGG only (wanglab/kegg): composes src/conf/data/bioreason_kegg_only.yaml, +# which forces the variant_effect_* datasets and every other default-on stream off. +defaults: + - /data@_global_.data: bioreason_kegg_only + - _self_ + task: "bioreason_grpo" batch_size: 8 max_steps: 1000 diff --git a/src/conf/training/bioreason_projector.yaml b/src/conf/training/bioreason_projector.yaml index be83035..b9a239a 100644 --- a/src/conf/training/bioreason_projector.yaml +++ b/src/conf/training/bioreason_projector.yaml @@ -3,13 +3,21 @@ # Run this before bioreason_sft.yaml (Stage 2). # Based on: https://arxiv.org/abs/2505.23579 +# KEGG only (wanglab/kegg): composes src/conf/data/bioreason_kegg_only.yaml, +# which forces the variant_effect_* datasets and every other default-on stream off. +defaults: + - /data@_global_.data: bioreason_kegg_only + - _self_ + task: "bioreason_projector" batch_size: 8 max_steps: 5000 # ~5 epochs over KEGG (1450 samples) device: "auto" learning_rate: 5e-5 gradient_accumulation_steps: 8 -eval_enabled: true # ZoneATrainer's periodic-eval block is opt-in; must be true for the KEGG eval below to fire +# Off: ZoneATrainer.run_kegg_eval imports eval_kegg.py, which is not in this repo, +# so the first eval step would raise ImportError and kill the run. +eval_enabled: false eval_every_n_steps: 200 # Also drives the KEGG validation eval below viz_every_n_steps: 1000 diff --git a/src/conf/training/bioreason_sft.yaml b/src/conf/training/bioreason_sft.yaml index e6432a0..65b5cda 100644 --- a/src/conf/training/bioreason_sft.yaml +++ b/src/conf/training/bioreason_sft.yaml @@ -1,20 +1,43 @@ -# BioReason Stage 2: SFT with LoRA -# Loads projector weights from Stage 1 (bioreason_projector.yaml) and freezes them. -# Trains only LoRA adapters on the LLM backbone. DNA encoder remains frozen. +# BioReason SFT with LoRA on KEGG (wanglab/kegg), 1B OLMo base. +# Single-stage, matching the BioReason repo (no projector pre-training stage): +# the DNA projector starts from its fresh random init and trains jointly with +# the LoRA adapters on the frozen LLM backbone. DNA encoder (NT-2.5B) remains +# frozen. Pass training.resume_weights_only= only to opt in to +# initializing from a pretrained projector (bioreason_projector.yaml). # Based on: https://arxiv.org/abs/2505.23579 # -# Before running: set resume_weights_only to the Stage 1 checkpoint path, e.g.: -# resume_weights_only: /path/to/stage1/outputs/.../checkpoints/step_final +# Must be composed with the 1B model (KEGG-only data comes from the defaults below): +# python src/train.py training=bioreason_sft model=prism_olmo_1b +# (config.yaml's default model is prism_olmo3_7b, so model=prism_olmo_1b is required.) +# +# Loss: labels are -100 everywhere except the assistant span +# <|assistant|>\n[ \n{reasoning}\n\n\nAnswer: {answer}<|endoftext|> ] +# (OLMo turn format: plain-text <|user|>/<|assistant|> headers, OLMo eos <|endoftext|> ends the target; +# no Qwen <|im_start|>/<|im_end|>.) +# (prompt/target split via the "P T" _metadata, requires model.is_interleaved_qa=true). +# Launcher: applications/bioreason/scripts/training/submit_bioreason_sft.sh + +# KEGG only (wanglab/kegg): composes src/conf/data/bioreason_kegg_only.yaml, +# which forces the variant_effect_* datasets and every other default-on stream off. +defaults: + - /data@_global_.data: bioreason_kegg_only + - _self_ task: "bioreason_sft" +# Per-rank batch 1 x accumulation 1 x 12 ranks = 12 samples/step (BioReason: 1 x 8 on 1 GPU). +# batch_size 8 at ~2.8-3.6k merged tokens/sample ran out of XPU memory (~40 s/micro-batch). +# accumulation 1 also keeps the scheduler units right: the trainer sizes the LR schedule as +# max_steps x num_ranks optimizer steps, which only matches when step == optimizer step. batch_size: 1 -max_steps: 10000 # ~10 epochs over KEGG (1450 train samples); previous 5000 steps showed no convergence trend +max_steps: 500 # ~5 epochs over the local KEGG shard (1159 train samples / 12 per step = ~97 steps/epoch), as in BioReason device: "auto" learning_rate: 5e-5 -gradient_accumulation_steps: 8 -eval_enabled: true # ZoneATrainer's periodic-eval block is opt-in; must be true for the KEGG eval below to fire +gradient_accumulation_steps: 1 +# Off: ZoneATrainer.run_kegg_eval imports eval_kegg.py, which is not in this repo, +# so the first eval step would raise ImportError and kill the run. +eval_enabled: false eval_every_n_steps: 1000 # Also drives the KEGG validation eval below -viz_every_n_steps: 1000 +viz_every_n_steps: 100 # KEGG validation eval (wanglab/kegg 'val' split), run inline during training on # the eval_every_n_steps cadence above. See ZoneATrainer.run_kegg_eval / @@ -22,31 +45,33 @@ viz_every_n_steps: 1000 kegg_eval_max_examples: null # null = full val split # Checkpointing -save_every_n_steps: 500 +save_every_n_steps: 100 resume_from_checkpoint: null -# Set this to the Stage 1 checkpoint directory to load projector weights. -# Loads model weights only — optimizer and step counter reset for the new stage. -resume_weights_only: ${oc.env:PRISM_OUTPUT_ROOT,}/GENOME-PROJECTOR-1B/2026-06-18/16-57-40/checkpoints/step_1500 +# Optional model-weights-only init (optimizer and step counter reset). null = +# projector starts from random init, as in BioReason. Set to a +# bioreason_projector (same model=prism_olmo_1b) checkpoint to start from a +# pretrained projector instead. +resume_weights_only: null output_dir: ${hydra:run.dir} # Optimization -warmup_steps: 300 # 3% of max_steps=10000; reduced from 500 — projector already pretrained +warmup_steps: 50 # 10% of max_steps=500, as in BioReason (get_cosine_schedule_with_warmup, 0.1 * total); projector is untrained weight_decay: 0.01 scheduler_type: "cosine" min_lr_ratio: 0.0 # Differential Learning Rates -lr_connector: 5e-5 # Projector LR +lr_connector: 5e-5 # Projector LR (random init, trains jointly with LoRA; same as BioReason's single lr 5e-5) lr_dna: 5e-6 # DNA encoder LR — only active if freeze_vit: false (frozen by default) -lr_llm: null # LLM frozen by default +lr_llm: null # Also the LoRA LR (null -> learning_rate); base LLM is frozen # Freezing # freeze_llm and lora_enabled are independent knobs: freeze_llm freezes the # base backbone, lora_enabled adds trainable LoRA adapters on top. Both true -# here -- base backbone frozen, only the LoRA adapters train. -freeze_llm: true # base backbone frozen; LoRA adapters (below) are the only trainable part +# here -- base backbone frozen, LoRA adapters + DNA projector train. +freeze_llm: true # base backbone frozen; LoRA adapters (below) train on top freeze_vit: true # DNA encoder frozen (matches BioReason default) -freeze_connector: true # projector loaded from Stage 1 and frozen; only LoRA trains +freeze_connector: false # projector randomly initialized and trained (lr_connector) # LoRA (applied to LLM backbone) lora_enabled: true @@ -61,7 +86,10 @@ use_reasoning_traces: true # Class-balanced loss: inverse-frequency weighting so rare diseases get proportional gradient. # Weights computed from wanglab/kegg train split at dataset init. # w_c = min(N / (C * n_c), class_weight_max) — capped to avoid n=1 instability. -use_class_weights: true +# Disabled: plain unweighted CE over the assistant span. (When true, the weights +# are counted via load_dataset("wanglab/kegg") from the Hub, which fails on +# offline compute nodes and silently falls back to uniform weights anyway.) +use_class_weights: false class_weight_max: 10.0 # Data diff --git a/src/config.py b/src/config.py index e9b6952..bbc8a6a 100644 --- a/src/config.py +++ b/src/config.py @@ -208,6 +208,10 @@ class ModelConfig: # DNA encoder settings dna_model_name: str = "InstaDeepAI/nucleotide-transformer-2.5b-multi-species" dna_is_evo2: bool = False # If True, DNAEncoder loads an Evo2 checkpoint instead of NT + # Fixed DNA span length in tokens. The data path pads every DNA span to exactly + # this (multimodal.py) and DNAEncoder sizes each interleaved slot from it, so + # both must read the same value. Set from training.max_dna_length in train.py. + max_dna_length: int = 1024 # Image encoder / processor image_encoder_id: str = "google/siglip2-base-patch16-224" diff --git a/src/data/datasets_config.json b/src/data/datasets_config.json index 76cef73..944fe10 100644 --- a/src/data/datasets_config.json +++ b/src/data/datasets_config.json @@ -418,7 +418,7 @@ "weight": 0.25, "handler": "dna_bioreason_variant_effect_coding", "modality": "dna", - "local_path": "${PRISM_DATA_ROOT}/dna_local_shards/variant_effect_coding", + "local_path": "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_coding", "preferred_source": "local", "required_keys": [ "question", @@ -427,7 +427,7 @@ "variant_sequence" ], "fallback_dummy": false, - "skip": false, + "skip": true, "size_formatted": "48,850 samples (train split)" }, "dna_bioreason_variant_effect_non_snv": { @@ -437,7 +437,7 @@ "weight": 0.25, "handler": "dna_bioreason_variant_effect_non_snv", "modality": "dna", - "local_path": "${PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv", + "local_path": "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_non_snv", "preferred_source": "local", "required_keys": [ "question", @@ -446,7 +446,7 @@ "mutated_sequence" ], "fallback_dummy": false, - "skip": false, + "skip": true, "size_formatted": "35,215 samples (train split)" } } diff --git a/src/data/multimodal.py b/src/data/multimodal.py index c1acfc8..5ccc972 100644 --- a/src/data/multimodal.py +++ b/src/data/multimodal.py @@ -210,14 +210,22 @@ def __init__( # NT tokenizer for DNA-LLM mode — loaded once here, not per item. # LLM mode inlines sequences as plain text and never needs this. + # Loaded from the same checkpoint as the DNA encoder: NT v1 (e.g. 2.5B) + # and NT v2 vocabularies order their 6-mers differently, so a tokenizer + # from another NT release feeds the encoder the wrong k-mer ids. self.dna_tokenizer = None if self.model_name == "dna-llm": + dna_tokenizer_id = getattr( + model_config, + "dna_model_name", + "InstaDeepAI/nucleotide-transformer-2.5b-multi-species", + ) try: self.dna_tokenizer = AutoTokenizer.from_pretrained( - "InstaDeepAI/nucleotide-transformer-v2-250m-multi-species", + dna_tokenizer_id, trust_remote_code=True, ) - logger.info("DNA tokenizer loaded: nucleotide-transformer-v2-250m-multi-species") + logger.info(f"DNA tokenizer loaded: {dna_tokenizer_id}") except Exception as e: logger.warning(f"Could not load DNA tokenizer: {e}") @@ -2663,17 +2671,22 @@ def _process_ts_time_mmd(self, item): def _render_dna_prompt_text(self, prompt: list) -> tuple: """ Renders the chat-format prompt list produced by _process_dna_bioreason into - two flat strings that match the Qwen/ChatML format expected by the tokenizer - and the interleaved merge machinery. + two flat strings in OLMo's own (Tulu-style) turn format -- NOT Qwen/ChatML. + The tokenizer here is OLMo's, which has no <|im_start|>/<|im_end|>: those + would be split into plain subword pieces, so the model could never emit a + real stop token. The only OLMo special token used is its eos + (self.tokenizer.eos_token, "<|endoftext|>"), which ends the assistant turn. + OLMo has no start/BOS token, so none is added; turns are introduced by the + plain-text headers "<|user|>" and "<|assistant|>". The label-masking logic in _merge_text_input_ids_with_modality_embeds locates the boundary between question and answer via the "P T" _metadata - token counts, while the collate fn scans for the literal substring - "<|im_end|>\\n<|im_start|>assistant\\n" to find where the assistant turn - begins. This renderer must produce exactly that substring. + token counts: P covers everything up to and including "<|assistant|>\\n", + T covers the assistant turn through eos (so eos is supervised and the model + learns to stop). User turn rendering: - <|im_start|>user\\n + <|user|>\\n [for each content slot] type=="dna_reference" → Reference sequence: \\n type=="dna_variant" → Variant sequence: \\n @@ -2681,22 +2694,41 @@ def _render_dna_prompt_text(self, prompt: list) -> tuple: modality_start_end_token_indices, NOT the chat-template <|dna_start|><|dna_pad|><|dna_end|> variant) type=="text" → the question text - <|im_end|>\\n + \\n Assistant turn rendering (SFT only): - <|im_start|>assistant\\n + <|assistant|>\\n [if reasoning] \\n{reasoning}\\n\\n\\n - {answer text} - <|im_end|>\\n + {answer text} (KEGG: "Answer: {answer}") + <|endoftext|> (OLMo eos; end of the loss span) Returns: full_text — user turn + assistant turn (use for example["text"]) - prompt_text — user turn only + "<|im_start|>assistant\\n" suffix + prompt_text — user turn only + "<|assistant|>\\n" suffix (use to measure prompt_tokens for _metadata) """ modality_tags = self.model_config.modality_start_end_token_indices or {} - dna_ref_tags = modality_tags.get("dna_reference", ("", "")) - dna_var_tags = modality_tags.get("dna_variant", ("", "")) + + # UnifiedTransformer.__init__ resolves these tag strings to integer ids + # *in place* on the shared ModelConfig, and train.py builds the dataset + # after the model. Rendering an int here would emit its digits as plain + # text ("Reference sequence: 5029250293"), so the merge never finds a + # splice point and the DNA embeddings are silently dropped. Map ids back + # to their token strings instead. + def _tag_str(tag): + if isinstance(tag, str): + return tag + return self.tokenizer.convert_ids_to_tokens(int(tag)) + + dna_ref_tags = tuple( + _tag_str(t) + for t in modality_tags.get("dna_reference", ("", "")) + ) + dna_var_tags = tuple( + _tag_str(t) + for t in modality_tags.get("dna_variant", ("", "")) + ) + eos = self.tokenizer.eos_token # OLMo "<|endoftext|>" dna_ref_placeholder = f"Reference sequence: {dna_ref_tags[0]}{dna_ref_tags[1]}\n" dna_var_placeholder = f"Variant sequence: {dna_var_tags[0]}{dna_var_tags[1]}\n" @@ -2720,25 +2752,27 @@ def _render_dna_prompt_text(self, prompt: list) -> tuple: parts.append(dna_var_placeholder) elif c.get("type") == "text": parts.append(c["text"]) - user_body = f"<|im_start|>user\n{''.join(parts)}<|im_end|>\n" + user_body = f"<|user|>\n{''.join(parts)}\n" elif role == "assistant": + # The sequence ends at eos with nothing after it: the SFT loss span is + # everything after prompt_text, so labels are exactly + # (<|assistant|>\n, eos] and nothing beyond. reasoning = msg.get("reasoning_content", "") answer = msg["content"][0]["text"] if reasoning: assistant_body = ( - f"<|im_start|>assistant\n" + f"<|assistant|>\n" f"\n{reasoning}\n\n\n" - f"{answer}<|im_end|>\n" + f"{answer}{eos}" ) else: - assistant_body = f"<|im_start|>assistant\n{answer}<|im_end|>\n" + assistant_body = f"<|assistant|>\n{answer}{eos}" # prompt_text ends exactly at the assistant marker so that: # len(tokenize(prompt_text)) == P (the P in the "P T" _metadata string) - # The marker "<|im_end|>\n<|im_start|>assistant\n" is the literal string - # relied on elsewhere to find where the assistant turn begins. - prompt_text = user_body + "<|im_start|>assistant\n" + # The marker "\n<|assistant|>\n" is the literal string that ends the prompt. + prompt_text = user_body + "<|assistant|>\n" # SFT: full_text = user turn + complete assistant turn (answer in the sequence). # RL: full_text = user turn + generation prompt only (model generates the answer). @@ -2933,14 +2967,15 @@ def _process_dna_bioreason(self, item): if self.is_sft: # SFT: append full assistant turn so collate fn can mask prompt tokens from loss. + # Target rendered by _render_dna_prompt_text: + # \n{reasoning}\n\n\nAnswer: {answer}<|endoftext|> + answer_text = f"Answer: {answer.strip()}" # Optionally truncate reasoning to stay within token budget. if self.use_reasoning_traces and reasoning and self.tokenizer is not None: MAX_SEQ = 2048 prompt_text = question_text prompt_len = len(self.tokenizer.encode(prompt_text, add_special_tokens=False)) - answer_overhead = len(self.tokenizer.encode( - f"{answer.strip()}", add_special_tokens=False - )) + answer_overhead = len(self.tokenizer.encode(answer_text, add_special_tokens=False)) reasoning_budget = MAX_SEQ - prompt_len - answer_overhead - 16 if reasoning_budget > 0: reasoning_ids = self.tokenizer.encode(reasoning, add_special_tokens=False) @@ -2954,7 +2989,7 @@ def _process_dna_bioreason(self, item): prompt.append({ "role": "assistant", "reasoning_content": reasoning.strip() if self.use_reasoning_traces else "", - "content": [{"type": "text", "text": f"{answer.strip()}"}], + "content": [{"type": "text", "text": answer_text}], }) # RL: no assistant turn — answer is kept as the reward signal only. @@ -3397,6 +3432,7 @@ def __iter__(self): "time_series": [], "geometry": [], "text": [], + "dna": [], } for name in list(self.datasets_objs.keys()): diff --git a/src/train.py b/src/train.py index 42579b1..820d8a9 100644 --- a/src/train.py +++ b/src/train.py @@ -255,71 +255,12 @@ def analyze_vla_dataset(dataset, train_config): logger.info("=" * 50 + "\n") -@hydra.main(config_path="conf", config_name="config", version_base="1.2") -def main(cfg: DictConfig): - if _is_main_env(): - print_prism_banner("Foundational Multimodal Training") - logger.info(f"--- Starting PRISM Training [{cfg.exp.id}] ---") - logger.info(OmegaConf.to_yaml(cfg)) - - # 0. Determine execution path - use_native_fsdp = os.environ.get("USE_NATIVE_FSDP", "0") == "1" - use_native_ddp = os.environ.get("USE_NATIVE_DDP", "0") == "1" - - # Defaults for Accelerate path (setup_distributed is native-only). - def _env_int(keys, default): - for key in keys: - val = os.environ.get(key, "") - if val and val.strip(): - try: - return int(val) - except ValueError: - continue - return default - - rank = _env_int(["RANK", "PMI_RANK", "PALS_RANKID"], 0) - world_size = _env_int(["WORLD_SIZE", "PMI_SIZE", "PALS_SIZE"], 1) - local_rank = _env_int(["LOCAL_RANK", "PMI_LOCAL_RANK", "PALS_LOCAL_RANKID"], 0) - device = cfg.training.get("device", "cpu") - - # Only call setup_distributed() for native DDP/FSDP paths. - # For Accelerate path, Accelerator handles distributed init internally. - # Calling both causes "Fatal error in internal_Init_thread" (double MPI init). - if use_native_fsdp or use_native_ddp: - # 0. Setup Distributed Environment (mpi4py-based for multi-node) - rank, world_size, local_rank, device = setup_distributed() - if rank == 0: - logger.info( - f"[Distributed] Rank {rank}/{world_size}, Local Rank {local_rank}, Device: {device}" - ) - - # Seed plumbing: `system.seed` lives in src/conf/config.yaml (default 42) but - # no code read it before. Stage A IsoFLOP's variance-floor measurement needs - # the BASE@3e17 seed-replicas to actually diverge, so seed every RNG the - # trainer touches. Set the same base seed on every rank — downstream - # samplers/collators add `rank` themselves where they need rank-specific - # streams (e.g. DistributedSampler), and starting from identical RNG state - # makes runs reproducible across world-size changes. - _seed_cfg = cfg.get("system", None) - seed = int(_seed_cfg.get("seed", 42)) if _seed_cfg is not None else 42 - random.seed(seed) - np.random.seed(seed) - torch.manual_seed(seed) - if hasattr(torch, "xpu") and torch.xpu.is_available(): - torch.xpu.manual_seed_all(seed) - if torch.cuda.is_available(): - torch.cuda.manual_seed_all(seed) - if rank == 0: - logger.info(f"[Seed] Set RNG state from system.seed={seed}") - - # 1. Configuration Mapping - from hydra.core.hydra_config import HydraConfig - - try: - hydra_out = HydraConfig.get().runtime.output_dir - except Exception: - hydra_out = os.getcwd() +def build_model_config(cfg: DictConfig, rank: int = 0) -> ModelConfig: + """Map the Hydra ``cfg.model`` / ``cfg.training`` tree onto a ``ModelConfig``. + Shared by ``main()`` and the notebooks/tools that need the exact model + ``src/train.py`` would build (e.g. applications/bioreason/bioreason_demo.ipynb). + """ ts_projector = str(cfg.model.get("ts_projector", "linear")) timeomni_patch_lens = _coerce_int_list(cfg.model.get("timeomni_patch_len", 16)) timeomni_strides = _resolve_timeomni_strides( @@ -371,6 +312,7 @@ def _env_int(keys, default): "dna_model_name", "InstaDeepAI/nucleotide-transformer-2.5b-multi-species" ), dna_is_evo2=cfg.model.get("dna_is_evo2", False), + max_dna_length=cfg.training.get("max_dna_length", 1024), image_encoder_id=cfg.model.get( "image_encoder_id", "google/siglip2-base-patch16-224" ), @@ -440,11 +382,14 @@ def _env_int(keys, default): if cfg.model.get("decoder_configs", None) is not None else {}, ) + return model_config - # Allow override of dimensions if specified in cfg (not implemented fully in hydra yaml yet, relying on defaults) - - +def build_training_config( + cfg: DictConfig, model_config: ModelConfig, output_dir: str, seed: int +) -> TrainingConfig: + """Map ``cfg.training`` onto a ``TrainingConfig``; checkpoints go to ``output_dir/checkpoints``.""" + hydra_out = output_dir train_config = TrainingConfig( batch_size=cfg.training.batch_size, max_steps=cfg.training.max_steps, @@ -526,10 +471,6 @@ def _env_int(keys, default): grpo_beta=cfg.training.get("grpo_beta", 0.0), grpo_reward_functions=cfg.training.get("grpo_reward_functions", None), ) - if train_config.wandb_mode: - os.environ["WANDB_MODE"] = train_config.wandb_mode - if train_config.wandb_entity: - os.environ["WANDB_ENTITY"] = train_config.wandb_entity # Inject eval_interval logic if getattr(cfg.training, "debug_mode", False) or cfg.training.max_steps < 100: @@ -545,6 +486,179 @@ def _env_int(keys, default): # gate never fires no matter what the YAML / CLI says. Surfaced during # the Smoke 4 replay on 2026-05-27. train_config.eval_enabled = getattr(cfg.training, "eval_enabled", False) + return train_config + + +def build_bio_dataset_kwargs( + task_type: str, train_config: TrainingConfig, model_config: ModelConfig +) -> dict: + """Extra ``StreamingMultimodalDataset`` kwargs for the BioReason DNA/KEGG tasks.""" + is_bio_task = task_type in ("bioreason_sft", "bioreason_grpo", "bioreason_projector") + # StreamingMultimodalDataset kwargs specific to BioReason DNA/KEGG runs. + # is_sft=False for the GRPO stage (no assistant turn appended; answer kept + # for reward computation), True for SFT/projector (assistant turn + # appended, loss masked to the answer). is_projector_only selects the + # "[dna_bioreason]" full-sequence-loss sentinel for the projector-only + # alignment stage, distinct from is_sft — see multimodal.py's + # _finalize_dna_item / model.py's label-masking branch. + bio_dataset_kwargs = ( + { + "use_reasoning_traces": train_config.use_reasoning_traces, + "model_name": "dna-llm" if Modality.DNA in model_config.modalities else "llm", + "is_sft": task_type != "bioreason_grpo", + "task": task_type, + "use_class_weights": train_config.use_class_weights, + "class_weight_max": train_config.class_weight_max, + "is_projector_only": task_type == "bioreason_projector", + } + if is_bio_task + else {} + ) + return bio_dataset_kwargs + + +def build_collator( + tokenizer, model_config: ModelConfig, use_bucketed_collator: bool, collator_max_seq: int +): + """Pick the Bucketed/Multimodal collator for the non-VLA streaming path.""" + _is_dynamic_length_ts = model_config.ts_projector in DYNAMIC_LENGTH_TS_PROJECTORS + if use_bucketed_collator and not _is_dynamic_length_ts: + collator = BucketedCollator( + tokenizer, + sort_within_batch=True, + log_efficiency=True, + max_seq_length=collator_max_seq, + ) + logger.info(f"Using BucketedCollator with max_seq_length={collator_max_seq}") + else: + if use_bucketed_collator and _is_dynamic_length_ts: + logger.info( + f"Disabling BucketedCollator for ts_projector={model_config.ts_projector}; " + "using MultimodalCollator with time_series passthrough for dynamic batching." + ) + # Interleaved-QA samples carry / token pairs and + # prompt/target metadata that must stay in sync; tail-truncating + # them at collate time would desync those invariants and crash the + # merge in model.py. That path is capped at the variate level in + # _process_ts_qa instead, so pass max_seq_length=None here. For + # plain (non-interleaved) batches, apply the same cap as the + # bucketed collator (issue #120). + collator_cap = None if model_config.is_interleaved_qa else collator_max_seq + collator = MultimodalCollator( + tokenizer, + max_seq_length=collator_cap, + passthrough_time_series=_is_dynamic_length_ts, + ) + logger.info( + f"Using MultimodalCollator with max_seq_length={collator_cap} " + f"(is_interleaved_qa={model_config.is_interleaved_qa}, " + f"passthrough_time_series={_is_dynamic_length_ts})" + ) + return collator + + +def build_streaming_dataset( + cfg: DictConfig, + tokenizer, + model_config: ModelConfig, + train_config: TrainingConfig, + bio_dataset_kwargs: dict, + max_seq_length: int, + allow_dummy_data: bool = False, +) -> StreamingMultimodalDataset: + """Build the HF/local-Arrow ``StreamingMultimodalDataset`` (the BioReason DNA path).""" + return StreamingMultimodalDataset( + tokenizer=tokenizer, + batch_size=train_config.batch_size, + max_steps=train_config.max_steps, + model_config=model_config, + hf_token=os.environ.get("HF_TOKEN"), + allow_dummy_data=allow_dummy_data, + verbosity=getattr( + logging, cfg.training.get("verbosity", "INFO").upper(), logging.INFO + ), + dataset_overrides=_resolve_dataset_overrides(cfg), + max_seq_length=max_seq_length, + **bio_dataset_kwargs, + ) + + +@hydra.main(config_path="conf", config_name="config", version_base="1.2") +def main(cfg: DictConfig): + if _is_main_env(): + print_prism_banner("Foundational Multimodal Training") + logger.info(f"--- Starting PRISM Training [{cfg.exp.id}] ---") + logger.info(OmegaConf.to_yaml(cfg)) + + # 0. Determine execution path + use_native_fsdp = os.environ.get("USE_NATIVE_FSDP", "0") == "1" + use_native_ddp = os.environ.get("USE_NATIVE_DDP", "0") == "1" + + # Defaults for Accelerate path (setup_distributed is native-only). + def _env_int(keys, default): + for key in keys: + val = os.environ.get(key, "") + if val and val.strip(): + try: + return int(val) + except ValueError: + continue + return default + + rank = _env_int(["RANK", "PMI_RANK", "PALS_RANKID"], 0) + world_size = _env_int(["WORLD_SIZE", "PMI_SIZE", "PALS_SIZE"], 1) + local_rank = _env_int(["LOCAL_RANK", "PMI_LOCAL_RANK", "PALS_LOCAL_RANKID"], 0) + device = cfg.training.get("device", "cpu") + + # Only call setup_distributed() for native DDP/FSDP paths. + # For Accelerate path, Accelerator handles distributed init internally. + # Calling both causes "Fatal error in internal_Init_thread" (double MPI init). + if use_native_fsdp or use_native_ddp: + # 0. Setup Distributed Environment (mpi4py-based for multi-node) + rank, world_size, local_rank, device = setup_distributed() + if rank == 0: + logger.info( + f"[Distributed] Rank {rank}/{world_size}, Local Rank {local_rank}, Device: {device}" + ) + + # Seed plumbing: `system.seed` lives in src/conf/config.yaml (default 42) but + # no code read it before. Stage A IsoFLOP's variance-floor measurement needs + # the BASE@3e17 seed-replicas to actually diverge, so seed every RNG the + # trainer touches. Set the same base seed on every rank — downstream + # samplers/collators add `rank` themselves where they need rank-specific + # streams (e.g. DistributedSampler), and starting from identical RNG state + # makes runs reproducible across world-size changes. + _seed_cfg = cfg.get("system", None) + seed = int(_seed_cfg.get("seed", 42)) if _seed_cfg is not None else 42 + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + if hasattr(torch, "xpu") and torch.xpu.is_available(): + torch.xpu.manual_seed_all(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(seed) + if rank == 0: + logger.info(f"[Seed] Set RNG state from system.seed={seed}") + + # 1. Configuration Mapping + from hydra.core.hydra_config import HydraConfig + + try: + hydra_out = HydraConfig.get().runtime.output_dir + except Exception: + hydra_out = os.getcwd() + + model_config = build_model_config(cfg, rank=rank) + + + # Allow override of dimensions if specified in cfg (not implemented fully in hydra yaml yet, relying on defaults) + + + train_config = build_training_config(cfg, model_config, hydra_out, seed) + if train_config.wandb_mode: + os.environ["WANDB_MODE"] = train_config.wandb_mode + if train_config.wandb_entity: + os.environ["WANDB_ENTITY"] = train_config.wandb_entity # Debug print _rprint( @@ -691,26 +805,7 @@ def _env_int(keys, default): raise RuntimeError("training.task=vla_calvin requires model.is_vla=true") is_bio_task = task_type in ("bioreason_sft", "bioreason_grpo", "bioreason_projector") - # StreamingMultimodalDataset kwargs specific to BioReason DNA/KEGG runs. - # is_sft=False for the GRPO stage (no assistant turn appended; answer kept - # for reward computation), True for SFT/projector (assistant turn - # appended, loss masked to the answer). is_projector_only selects the - # "[dna_bioreason]" full-sequence-loss sentinel for the projector-only - # alignment stage, distinct from is_sft — see multimodal.py's - # _finalize_dna_item / model.py's label-masking branch. - bio_dataset_kwargs = ( - { - "use_reasoning_traces": train_config.use_reasoning_traces, - "model_name": "dna-llm" if Modality.DNA in model_config.modalities else "llm", - "is_sft": task_type != "bioreason_grpo", - "task": task_type, - "use_class_weights": train_config.use_class_weights, - "class_weight_max": train_config.class_weight_max, - "is_projector_only": task_type == "bioreason_projector", - } - if is_bio_task - else {} - ) + bio_dataset_kwargs = build_bio_dataset_kwargs(task_type, train_config, model_config) # 3. Tokenizer / Data _rprint("Initializing Tokenizer...", flush=True) @@ -922,39 +1017,9 @@ def __call__(self, text, **kwargs): # from outlier-length sequences at epoch boundaries. # For projector training, 512 is sufficient; for E2E, match max_seq_length. collator_max_seq = int(os.environ.get("MAX_SEQ_LENGTH", "2048")) - _is_dynamic_length_ts = model_config.ts_projector in DYNAMIC_LENGTH_TS_PROJECTORS - if use_bucketed_collator and not _is_dynamic_length_ts: - collator = BucketedCollator( - tokenizer, - sort_within_batch=True, - log_efficiency=True, - max_seq_length=collator_max_seq, - ) - logger.info(f"Using BucketedCollator with max_seq_length={collator_max_seq}") - else: - if use_bucketed_collator and _is_dynamic_length_ts: - logger.info( - f"Disabling BucketedCollator for ts_projector={model_config.ts_projector}; " - "using MultimodalCollator with time_series passthrough for dynamic batching." - ) - # Interleaved-QA samples carry / token pairs and - # prompt/target metadata that must stay in sync; tail-truncating - # them at collate time would desync those invariants and crash the - # merge in model.py. That path is capped at the variate level in - # _process_ts_qa instead, so pass max_seq_length=None here. For - # plain (non-interleaved) batches, apply the same cap as the - # bucketed collator (issue #120). - collator_cap = None if model_config.is_interleaved_qa else collator_max_seq - collator = MultimodalCollator( - tokenizer, - max_seq_length=collator_cap, - passthrough_time_series=_is_dynamic_length_ts, - ) - logger.info( - f"Using MultimodalCollator with max_seq_length={collator_cap} " - f"(is_interleaved_qa={model_config.is_interleaved_qa}, " - f"passthrough_time_series={_is_dynamic_length_ts})" - ) + collator = build_collator( + tokenizer, model_config, use_bucketed_collator, collator_max_seq + ) # Check if we should use direct MultiWebDataset mode # This bypasses StreamingMultimodalDataset which has integration issues # with multi-dataset loading (Issue 14 in the Feb-2026 bring-up debug @@ -1201,35 +1266,15 @@ def __call__(self, text, **kwargs): f"WARNING: LOCAL_SHARDS_DIR={local_shards_dir} exists but has no .tar files, falling back to StreamingMultimodalDataset", flush=True, ) - dataset = StreamingMultimodalDataset( - tokenizer=tokenizer, - batch_size=train_config.batch_size, - max_steps=train_config.max_steps, - model_config=model_config, - hf_token=os.environ.get("HF_TOKEN"), - allow_dummy_data=_enable_all_modalities, - verbosity=getattr( - logging, cfg.training.get("verbosity", "INFO").upper(), logging.INFO - ), - dataset_overrides=_resolve_dataset_overrides(cfg), - max_seq_length=collator_max_seq, - **bio_dataset_kwargs, + dataset = build_streaming_dataset( + cfg, tokenizer, model_config, train_config, bio_dataset_kwargs, + max_seq_length=collator_max_seq, allow_dummy_data=_enable_all_modalities, ) else: # Pure legacy mode: Use StreamingMultimodalDataset - dataset = StreamingMultimodalDataset( - tokenizer=tokenizer, - batch_size=train_config.batch_size, - max_steps=train_config.max_steps, - model_config=model_config, - hf_token=os.environ.get("HF_TOKEN"), - allow_dummy_data=_enable_all_modalities, - verbosity=getattr( - logging, cfg.training.get("verbosity", "INFO").upper(), logging.INFO - ), - dataset_overrides=_resolve_dataset_overrides(cfg), - max_seq_length=collator_max_seq, - **bio_dataset_kwargs, + dataset = build_streaming_dataset( + cfg, tokenizer, model_config, train_config, bio_dataset_kwargs, + max_seq_length=collator_max_seq, allow_dummy_data=_enable_all_modalities, ) # Compare the modalities the dataloader will actually emit against diff --git a/src/training/distributed.py b/src/training/distributed.py index f096359..9158330 100644 --- a/src/training/distributed.py +++ b/src/training/distributed.py @@ -1127,7 +1127,9 @@ def load_model_weights_only(model, checkpoint_path, device="cpu"): ) print(f"[Resume] Loading model weights from {safetensors_path}") - state_dict = load_file(safetensors_path, device=str(device)) + # Load on CPU: `device` may be the string "auto" (training.device), which + # safetensors rejects; load_state_dict copies onto the model's device anyway. + state_dict = load_file(safetensors_path, device="cpu") # Strip "module." prefix if present (from DDP-saved checkpoints) clean_state_dict = {} diff --git a/src/training/trainer_zone_a.py b/src/training/trainer_zone_a.py index cb76e45..22ed975 100644 --- a/src/training/trainer_zone_a.py +++ b/src/training/trainer_zone_a.py @@ -394,15 +394,25 @@ def __init__(self, model: UnifiedTransformer, config: TrainingConfig, train_load sched_type = getattr(config, "scheduler_type", "cosine") min_lr_ratio = getattr(config, "min_lr_ratio", 0.0) + # accelerator.prepare(scheduler) below wraps it in AcceleratedScheduler, + # which calls the inner scheduler.step() num_processes times per + # optimizer step (it assumes the dataloader batch was scaled by world + # size). Express every schedule length in those units so warmup/decay + # are counted in optimizer steps; otherwise on 12 ranks warmup ends 12x + # early and the HF cosine passes its end at max_steps/12 and rises again. + n_sched = self.accelerator.num_processes + sched_total = config.max_steps * n_sched + sched_warmup = config.warmup_steps * n_sched + if sched_type == "molmo_layered": from src.utils.scheduler import get_molmo_scheduler self.accelerator.print("Using Molmo Layered Scheduler (Separate Warmups)") self.scheduler = get_molmo_scheduler( self.optimizer, - config.max_steps, - warmup_connector=getattr(config, "warmup_steps_connector", 200), - warmup_main=getattr(config, "warmup_steps_main", 2000), + sched_total, + warmup_connector=getattr(config, "warmup_steps_connector", 200) * n_sched, + warmup_main=getattr(config, "warmup_steps_main", 2000) * n_sched, min_lr_ratio=min_lr_ratio, ) elif sched_type == "cosine_with_min_lr": @@ -411,8 +421,8 @@ def __init__(self, model: UnifiedTransformer, config: TrainingConfig, train_load self.accelerator.print(f"Using Cosine Scheduler with Floor {min_lr_ratio}") self.scheduler = get_cosine_with_min_lr( self.optimizer, - num_warmup_steps=config.warmup_steps, - num_training_steps=config.max_steps, + num_warmup_steps=sched_warmup, + num_training_steps=sched_total, min_lr_ratio=min_lr_ratio, ) elif sched_type == "wsd": @@ -427,18 +437,18 @@ def __init__(self, model: UnifiedTransformer, config: TrainingConfig, train_load ) self.scheduler = get_wsd_scheduler( self.optimizer, - num_warmup_steps=config.warmup_steps, - num_training_steps=config.max_steps, + num_warmup_steps=sched_warmup, + num_training_steps=sched_total, min_lr_ratio=min_lr_ratio, decay_ratio=getattr(config, "wsd_decay_ratio", 0.1), - decay_steps=wsd_decay_steps, + decay_steps=wsd_decay_steps * n_sched if wsd_decay_steps else wsd_decay_steps, ) else: # Default HF self.scheduler = get_cosine_schedule_with_warmup( self.optimizer, - num_warmup_steps=config.warmup_steps, - num_training_steps=config.max_steps, + num_warmup_steps=sched_warmup, + num_training_steps=sched_total, ) # 3. Prepare via Accelerator @@ -603,6 +613,33 @@ def add_evaluator(name, registry_key): self._kegg_eval_logger = None self._kegg_eval_nt_tokenizer = None + # Parameter-name keys -> label, shared by the gradient watchdog and its snapshot. + _GRAD_COMPONENTS = ( + ("projector", "Projector"), + ("lora_", "LoRA"), + ("encoder", "Encoder/ViT"), + ("backbone", "Backbone/LLM"), + ) + + def _snapshot_grad_norms(self): + """Record the largest grad norm per component (label -> (norm, param name)). + + Called just before ``optimizer.zero_grad()`` on steps whose *next* iteration runs + ``check_parameter_status``: that check runs at the top of the loop, after + ``zero_grad()`` has already set every ``.grad`` to None, so it can't see them. + """ + snap = {} + for key, label in self._GRAD_COMPONENTS: + best, name = 0.0, None + for n, p in self.model.named_parameters(): + if key in n and p.requires_grad and p.grad is not None: + g = p.grad.norm().item() + if g > best: + best, name = g, n + if name is not None: + snap[label] = (best, name) + self._grad_snapshot = snap + def check_parameter_status(self, step): """Watchdog: Fail fast if projectors are seemingly frozen.""" if not self.accelerator.is_main_process: @@ -672,12 +709,8 @@ def check_parameter_status(self, step): # 2. Check Gradients (Only if step > 0) # Sample from each component type if step > 0: - components = [ - ("projector", "Projector"), - ("lora_", "LoRA"), - ("encoder", "Encoder/ViT"), - ("backbone", "Backbone/LLM"), - ] + components = self._GRAD_COMPONENTS + snapshot = getattr(self, "_grad_snapshot", None) or {} for key, label in components: found_grad = False @@ -692,9 +725,17 @@ def check_parameter_status(self, step): sample_name = n found_grad = True + from_snapshot = False + if not found_grad and label in snapshot: + # Live grads were already cleared by zero_grad(); use the norms + # recorded just before it on the previous optimizer step. + max_norm, sample_name = snapshot[label] + found_grad, from_snapshot = True, True + if found_grad and max_norm > 0: + src = " [last optimizer step]" if from_snapshot else "" print( - f" ✅ {label}: {sample_name.split('.')[-2]}.{sample_name.split('.')[-1]} (Norm: {max_norm:.6f})" + f" ✅ {label}: {sample_name.split('.')[-2]}.{sample_name.split('.')[-1]} (Norm: {max_norm:.6f}){src}" ) elif found_grad: print(f" ⚠️ {label}: Gradients exist but are zero!") @@ -1059,6 +1100,10 @@ def train(self): t3 = time.perf_counter() self.optimizer.step() self.scheduler.step() + # The watchdog runs at the top of iteration step+1 (step 1 and every + # 100th), after zero_grad(); capture the grad norms it will report. + if (step + 1) == 1 or (step + 1) % 100 == 0: + self._snapshot_grad_norms() self.optimizer.zero_grad() # Skipped under PRISM_PRODUCTION_MODE if not _production_mode and hasattr(torch, "xpu") and torch.xpu.is_available(): @@ -1428,8 +1473,9 @@ def run_kegg_eval(self, step): if dna_encoder is not None and getattr(dna_encoder, "tokenizer", None) is not None: self._kegg_eval_nt_tokenizer = dna_encoder.tokenizer else: + # Same checkpoint as the encoder (see StreamingMultimodalDataset). self._kegg_eval_nt_tokenizer = AutoTokenizer.from_pretrained( - "InstaDeepAI/nucleotide-transformer-v2-250m-multi-species", + unwrapped.config.dna_model_name, trust_remote_code=True, ) diff --git a/tools/ci/site_path_allowlist.json b/tools/ci/site_path_allowlist.json index 89db34d..663b241 100644 --- a/tools/ci/site_path_allowlist.json +++ b/tools/ci/site_path_allowlist.json @@ -1,11 +1,11 @@ { "_comment": "Per-file counts of hardcoded site paths, as a ratchet. See tools/ci/check_site_paths.py. Counts may only decrease; regenerate with `python tools/ci/check_site_paths.py --write`.", "files": { - "applications/bioreason/submit_bioreason_grpo.sh": 8, - "applications/bioreason/submit_bioreason_projector.sh": 7, - "applications/bioreason/submit_bioreason_sft.sh": 10, - "applications/bioreason/submit_eval_kegg.sh": 8, - "applications/bioreason/verify_dna_dataset_mix.sh": 7, + "applications/bioreason/scripts/training/submit_bioreason_grpo.sh": 8, + "applications/bioreason/scripts/training/submit_bioreason_projector.sh": 7, + "applications/bioreason/scripts/training/submit_bioreason_sft.sh": 10, + "applications/bioreason/scripts/eval/submit_eval_kegg.sh": 8, + "applications/bioreason/scripts/test/verify_dna_dataset_mix.sh": 7, "applications/material_science/stage_materials_text_model.py": 1, "applications/text/convert_arxiv_streaming.py": 6, "applications/text/convert_nemotron_to_webdataset.py": 4, From 66bcc87747701dd90f859515c021be256342d616 Mon Sep 17 00:00:00 2001 From: Adarsha Balaji Date: Tue, 13 Oct 2026 18:44:22 +0000 Subject: [PATCH 2/8] bioreason: KEGG-only via datasets_config.json; drop bioreason_kegg_only.yaml Only dna_bioreason (wanglab/kegg) is enabled in zone_a (ts_qa now skip=true), so the per-run data override and the training presets' defaults lists are no longer needed. Co-Authored-By: Claude Sonnet 5.5 --- src/conf/data/bioreason_demo_mix.yaml | 2 -- src/conf/data/bioreason_kegg_only.yaml | 16 ---------------- src/conf/training/bioreason_grpo.yaml | 6 ------ src/conf/training/bioreason_projector.yaml | 8 +------- src/conf/training/bioreason_sft.yaml | 10 ++-------- src/data/datasets_config.json | 2 +- 6 files changed, 4 insertions(+), 40 deletions(-) delete mode 100644 src/conf/data/bioreason_kegg_only.yaml diff --git a/src/conf/data/bioreason_demo_mix.yaml b/src/conf/data/bioreason_demo_mix.yaml index 0de6412..4794d9a 100644 --- a/src/conf/data/bioreason_demo_mix.yaml +++ b/src/conf/data/bioreason_demo_mix.yaml @@ -15,5 +15,3 @@ dataset_overrides: preferred_source: local weight: 0.25 local_path: ${oc.env:PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv - ts_qa: - skip: true diff --git a/src/conf/data/bioreason_kegg_only.yaml b/src/conf/data/bioreason_kegg_only.yaml deleted file mode 100644 index 8b74eec..0000000 --- a/src/conf/data/bioreason_kegg_only.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# BioReason KEGG-only data mix (wanglab/kegg via the dna_bioreason handler). -# Pins every zone_a dataset that is default-on in datasets_config.json off, so -# the SFT run cannot silently mix in another stream (ts_qa is skip=false there). -# -# Composed automatically by training=bioreason_{projector,sft,grpo} via their -# defaults list (/data@_global_.data) -- do not also pass +data=bioreason_kegg_only. -dataset_overrides: - dna_bioreason: - skip: false - preferred_source: local - dna_bioreason_variant_effect_coding: - skip: true - dna_bioreason_variant_effect_non_snv: - skip: true - ts_qa: - skip: true diff --git a/src/conf/training/bioreason_grpo.yaml b/src/conf/training/bioreason_grpo.yaml index 540f3ce..35e4178 100644 --- a/src/conf/training/bioreason_grpo.yaml +++ b/src/conf/training/bioreason_grpo.yaml @@ -2,12 +2,6 @@ # Based on: https://arxiv.org/abs/2505.23579 # Stage 2: Group Relative Policy Optimization for biological reasoning -# KEGG only (wanglab/kegg): composes src/conf/data/bioreason_kegg_only.yaml, -# which forces the variant_effect_* datasets and every other default-on stream off. -defaults: - - /data@_global_.data: bioreason_kegg_only - - _self_ - task: "bioreason_grpo" batch_size: 8 max_steps: 1000 diff --git a/src/conf/training/bioreason_projector.yaml b/src/conf/training/bioreason_projector.yaml index b9a239a..ae0f4f8 100644 --- a/src/conf/training/bioreason_projector.yaml +++ b/src/conf/training/bioreason_projector.yaml @@ -3,12 +3,6 @@ # Run this before bioreason_sft.yaml (Stage 2). # Based on: https://arxiv.org/abs/2505.23579 -# KEGG only (wanglab/kegg): composes src/conf/data/bioreason_kegg_only.yaml, -# which forces the variant_effect_* datasets and every other default-on stream off. -defaults: - - /data@_global_.data: bioreason_kegg_only - - _self_ - task: "bioreason_projector" batch_size: 8 max_steps: 5000 # ~5 epochs over KEGG (1450 samples) @@ -58,7 +52,7 @@ use_reasoning_traces: false data_num_workers: 1 # BioReason-specific -bioreason_dataset: "wanglab/kegg" # or variant_effect_coding, variant_effect_non_snv +bioreason_dataset: "wanglab/kegg" max_dna_length: 1024 max_text_length: 1024 dna_truncation_per_side: 1024 diff --git a/src/conf/training/bioreason_sft.yaml b/src/conf/training/bioreason_sft.yaml index 65b5cda..a439b8d 100644 --- a/src/conf/training/bioreason_sft.yaml +++ b/src/conf/training/bioreason_sft.yaml @@ -6,7 +6,7 @@ # initializing from a pretrained projector (bioreason_projector.yaml). # Based on: https://arxiv.org/abs/2505.23579 # -# Must be composed with the 1B model (KEGG-only data comes from the defaults below): +# Must be composed with the 1B model (KEGG-only comes from datasets_config.json, where only dna_bioreason is enabled): # python src/train.py training=bioreason_sft model=prism_olmo_1b # (config.yaml's default model is prism_olmo3_7b, so model=prism_olmo_1b is required.) # @@ -17,12 +17,6 @@ # (prompt/target split via the "P T" _metadata, requires model.is_interleaved_qa=true). # Launcher: applications/bioreason/scripts/training/submit_bioreason_sft.sh -# KEGG only (wanglab/kegg): composes src/conf/data/bioreason_kegg_only.yaml, -# which forces the variant_effect_* datasets and every other default-on stream off. -defaults: - - /data@_global_.data: bioreason_kegg_only - - _self_ - task: "bioreason_sft" # Per-rank batch 1 x accumulation 1 x 12 ranks = 12 samples/step (BioReason: 1 x 8 on 1 GPU). # batch_size 8 at ~2.8-3.6k merged tokens/sample ran out of XPU memory (~40 s/micro-batch). @@ -96,7 +90,7 @@ class_weight_max: 10.0 data_num_workers: 1 # BioReason-specific -bioreason_dataset: "wanglab/kegg" # or variant_effect_coding, variant_effect_non_snv +bioreason_dataset: "wanglab/kegg" max_dna_length: 1024 max_text_length: 1024 dna_truncation_per_side: 1024 diff --git a/src/data/datasets_config.json b/src/data/datasets_config.json index 944fe10..0891326 100644 --- a/src/data/datasets_config.json +++ b/src/data/datasets_config.json @@ -196,7 +196,7 @@ "output" ], "fallback_dummy": false, - "skip": false, + "skip": true, "size_formatted": "~105K samples" }, "ts_instruction": { From 7101adfb668e010e3a9fd1988904dfdf3f130cc6 Mon Sep 17 00:00:00 2001 From: Adarsha Balaji Date: Tue, 13 Oct 2026 19:04:11 +0000 Subject: [PATCH 3/8] datasets_config: use ${PRISM_DATA_ROOT} for variant-effect local_path Replaces the two hardcoded /lus/flare/... user paths with the site variable. Co-Authored-By: Claude Sonnet 5.5 --- src/data/datasets_config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/datasets_config.json b/src/data/datasets_config.json index 0891326..0d1de26 100644 --- a/src/data/datasets_config.json +++ b/src/data/datasets_config.json @@ -418,7 +418,7 @@ "weight": 0.25, "handler": "dna_bioreason_variant_effect_coding", "modality": "dna", - "local_path": "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_coding", + "local_path": "${PRISM_DATA_ROOT}/dna_local_shards/variant_effect_coding", "preferred_source": "local", "required_keys": [ "question", @@ -437,7 +437,7 @@ "weight": 0.25, "handler": "dna_bioreason_variant_effect_non_snv", "modality": "dna", - "local_path": "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_non_snv", + "local_path": "${PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv", "preferred_source": "local", "required_keys": [ "question", From a44346b30e6c4487097c888f0619d80aeb052f9e Mon Sep 17 00:00:00 2001 From: Adarsha Balaji Date: Tue, 13 Oct 2026 19:04:12 +0000 Subject: [PATCH 4/8] bioreason_projector.yaml: set demo hyperparameters batch_size 1, max_steps 100, warmup 10, grad-accum 1, save every 50. The projector launcher now takes these from the config instead of forcing its own defaults. Co-Authored-By: Claude Sonnet 5.5 --- src/conf/training/bioreason_projector.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/conf/training/bioreason_projector.yaml b/src/conf/training/bioreason_projector.yaml index ae0f4f8..f5a4eb3 100644 --- a/src/conf/training/bioreason_projector.yaml +++ b/src/conf/training/bioreason_projector.yaml @@ -4,11 +4,11 @@ # Based on: https://arxiv.org/abs/2505.23579 task: "bioreason_projector" -batch_size: 8 -max_steps: 5000 # ~5 epochs over KEGG (1450 samples) +batch_size: 1 # batch 8 at ~3k merged tokens (1024 DNA slots x2 + text) OOMs an Aurora tile +max_steps: 100 # demo-sized run; raise for a full alignment (the SFT-stage runs used 500) device: "auto" learning_rate: 5e-5 -gradient_accumulation_steps: 8 +gradient_accumulation_steps: 1 # Off: ZoneATrainer.run_kegg_eval imports eval_kegg.py, which is not in this repo, # so the first eval step would raise ImportError and kill the run. eval_enabled: false @@ -21,13 +21,13 @@ viz_every_n_steps: 1000 kegg_eval_max_examples: null # null = full val split # Checkpointing -save_every_n_steps: 500 +save_every_n_steps: 50 resume_from_checkpoint: null resume_weights_only: null output_dir: ${hydra:run.dir} # Optimization -warmup_steps: 500 # 10% of max_steps=5000 +warmup_steps: 10 # 10% of max_steps=100 weight_decay: 0.01 scheduler_type: "cosine" min_lr_ratio: 0.0 From b4ba312e4d1e47898e2bccede8d25d3649ba9492 Mon Sep 17 00:00:00 2001 From: Adarsha Balaji Date: Tue, 13 Oct 2026 19:09:06 +0000 Subject: [PATCH 5/8] bioreason: site-neutral launchers/notebook, config-driven hyperparameters, split PRISM and Jupyter setup - PROJ_DIR derived from the script location (no home-directory paths); BaseMM_PRISM -> PRISM - no hardcoded PBS allocation (qsub -A ; demo_helpers.qsub gains account=) - placeholder in default site paths; notebook outputs sanitized - projector/SFT/GRPO launchers pass training.* overrides only when env vars are set - helpers/setup_prism_env.sh (PRISM venv) separated from setup_jupyter_kernel.sh (Jupyter + kernel) - notebook: rewritten setup cells, KEGG-only section 5 Co-Authored-By: Claude Sonnet 5.5 --- applications/bioreason/bioreason_demo.ipynb | 289 +++++++++--------- .../bioreason/helpers/demo_helpers.py | 6 +- .../bioreason/helpers/kernel_wrapper.sh | 2 +- .../bioreason/helpers/setup_jupyter_kernel.sh | 33 +- .../bioreason/helpers/setup_prism_env.sh | 43 +++ .../scripts/eval/submit_eval_kegg.sh | 6 +- .../scripts/eval/submit_eval_kegg_aurora.sh | 10 +- .../scripts/eval/submit_eval_sft_sweep.sh | 2 +- .../scripts/test/verify_dna_dataset_mix.sh | 6 +- .../scripts/training/submit_bioreason_grpo.sh | 6 +- .../training/submit_bioreason_grpo_aurora.sh | 32 +- .../training/submit_bioreason_projector.sh | 46 +-- .../scripts/training/submit_bioreason_sft.sh | 28 +- 13 files changed, 280 insertions(+), 229 deletions(-) create mode 100755 applications/bioreason/helpers/setup_prism_env.sh diff --git a/applications/bioreason/bioreason_demo.ipynb b/applications/bioreason/bioreason_demo.ipynb index b472489..1c297f4 100644 --- a/applications/bioreason/bioreason_demo.ipynb +++ b/applications/bioreason/bioreason_demo.ipynb @@ -52,34 +52,49 @@ "id": "cell-env-install", "metadata": {}, "source": [ - "## Environment installation (once, in a terminal, before opening this notebook)\n", + "## Setup for a new Aurora user (do this once, outside the notebook)\n", "\n", - "The notebook's kernel *is* this environment, so build it from a shell first. It follows the README's Aurora rules (`uv`-managed venv on `/flare`, PRISM installed with `--no-deps` so pip never replaces the module's XPU PyTorch, frameworks module loaded **before** the venv), with one difference noted at the end.\n", + "Two independent parts. **Part A** builds the PRISM environment; it has nothing to do with Jupyter, and the training scripts use it directly. **Part B** adds Jupyter on top of it for this notebook. Skip B if you only submit jobs from the shell.\n", + "\n", + "### Part A. PRISM environment (login node, internet access)\n", + "\n", + "It follows the README's Aurora rules: a `uv`-managed venv on `/flare`, the frameworks module loaded **before** the venv, and PRISM installed with `--no-deps` so pip never replaces the module's XPU PyTorch.\n", "\n", - "**① On a login node** (it has direct internet access):\n", "```bash\n", - "# uv (README: required by the Aurora env builders)\n", + "# Your PBS allocation (the project you pass to qsub -A). Reused by every step below.\n", + "export PROJECT=\n", + "\n", + "# uv, the package manager the venv builders use (skip if `uv --version` already works)\n", "curl -LsSf https://astral.sh/uv/install.sh | sh\n", + "source \"$HOME/.local/bin/env\" # puts uv on PATH in this shell\n", "\n", - "# code\n", - "git clone https://github.com/AI-ModCon/BaseMM_PRISM.git\n", - "cd BaseMM_PRISM && git checkout bioreason_tutorial\n", + "# Code\n", + "cd /flare/$PROJECT/$USER # or any directory with room for a clone\n", + "git clone https://github.com/AI-ModCon/PRISM.git\n", + "cd PRISM && git checkout prism-bioreason-demo\n", + "\n", + "# Environment: loads frameworks/2026.1.0, creates a --system-site-packages venv at $PRISM_VENV,\n", + "# installs the extras the module lacks (torchtune, webdataset, wandb) and PRISM itself (--no-deps).\n", + "export PRISM_VENV=/flare/$PROJECT/$USER/prism-envs/bioreason-fw2026.1\n", + "bash applications/bioreason/helpers/setup_prism_env.sh\n", + "```\n", "\n", - "# venv + Jupyter kernel: loads frameworks/2026.1.0, creates a --system-site-packages\n", - "# venv at /flare/ModCon/$USER/prism-envs/bioreason-fw2026.1, installs the extras the module\n", - "# lacks (torchtune, webdataset, wandb, ipykernel, ipywidgets, jupyter_server) with --no-deps,\n", - "# installs PRISM itself with --no-deps, and registers the \"PRISM BioReason (Aurora)\" kernel.\n", - "bash applications/bioreason/helpers/setup_jupyter_kernel.sh\n", + "> **Why not `tools/build_aurora_env.sh`?** The README's canonical Aurora build pins `frameworks/2025.3.1` and its lockfile. The BioReason scripts and this tutorial run on **`frameworks/2026.1.0`**, the stack everything here was tested on, so they use their own venv. Don't mix the two: a venv built against one frameworks module and loaded under the other fails with import or ABI errors. Note that 2026.1.0 ships transformers 5.x, so only Nucleotide Transformer **v1** checkpoints (e.g. the 2.5B multi-species one used here) load. NT-v2's remote code doesn't.\n", "\n", - "# on the compute node\n", - "module use /soft/modulefiles && module load frameworks/2026.1.0 # module first ...\n", + "### Part B. Jupyter for this notebook (login node)\n", + "\n", + "Jupyter is **not** part of PRISM's requirements. This step installs `ipykernel`, `ipywidgets` and `jupyter_server` into the Part A venv and registers the \"PRISM BioReason (Aurora)\" kernel, which always starts with the module and venv loaded.\n", + "\n", + "```bash\n", + "bash applications/bioreason/helpers/setup_jupyter_kernel.sh # same shell: PRISM_VENV is still exported\n", + "```\n", "\n", - "python -m venv .venv && source .venv/bin/activate\n", - "pip install -r requirements/base.txt\n", - "pip install -e . --no-deps\n", + "Check it (still on the login node):\n", "\n", - "source /flare/ModCon/$USER/$PROJECT/BaseMM_PRISM/.venv/bin/activate # ... then the venv\n", - "cd /flare/ModCon/$USER/$PROJECT/BaseMM_PRISM # wherever you cloned it" + "```bash\n", + "ls ~/.local/share/jupyter/kernels/prism-bioreason/kernel.json # the registered kernel\n", + "source $PRISM_VENV/bin/activate && jupyter server --version # Jupyter is installed in the venv\n", + "```\n" ] }, { @@ -87,25 +102,31 @@ "id": "fd6c415d", "metadata": {}, "source": [ - "# Connect Jupyter notebook to interactive compute node. \n", + "### Part B (continued). Start a Jupyter server on a compute node\n", "\n", - "** Get a compute node and start a Jupyter server on it:**\n", - "```\n", + "Models and training need an XPU, so run the server on a compute node. Request one **from the login node**:\n", "\n", "```bash\n", + "qsub -I -l select=1 -l walltime=1:00:00 -q debug -A $PROJECT -l filesystems=home:flare\n", + "```\n", "\n", - "qsub -I -l select=1 -l walltime=1:00:00 -q debug -A ModCon -l filesystems=home:flare\n", - "\n", - "source /flare/ModCon/$USER/$PROJECT/BaseMM_PRISM/.venv/bin/activate # ... then the venv\n", + "Once the job starts you are on the compute node. Load the environment **in this order** (module first, then the venv) and start the server:\n", "\n", - "jupyter server --no-browser --ip=$(hostname -f) --port 8888 # prints the URL + a new random token\n", + "```bash\n", + "export PROJECT= # the notebook reads this to build its paths and to qsub with -A\n", + "export PRISM_VENV=/flare/$PROJECT/$USER/prism-envs/bioreason-fw2026.1\n", + "module use /soft/modulefiles && module load frameworks/2026.1.0 # module first ...\n", + "source $PRISM_VENV/bin/activate # ... then the venv\n", "\n", + "cd /flare/$PROJECT/$USER/PRISM # the clone from Part A\n", + "jupyter server --no-browser --ip=$(hostname -f) --port 8888 # prints the URL + a new random token\n", "```\n", "\n", - "Lost the Jupyter URL? \n", - "Run `jupyter server list` on the node. \n", + "The kernel inherits this shell's environment, so `PROJECT` has to be exported **before** the server starts.\n", + "\n", + "Lost the Jupyter URL? Run `jupyter server list` on the node.\n", "\n", - "To reuse **one fixed token** across nodes, create it once with \n", + "To reuse **one fixed token** across nodes, create it once with\n", "`python3 -c \"import secrets; print(secrets.token_hex(24))\" > ~/.jupyter_token && chmod 600 ~/.jupyter_token`, then start the server with:\n", "\n", "```bash\n", @@ -113,17 +134,17 @@ " --IdentityProvider.token=\"$(cat ~/.jupyter_token)\"\n", "```\n", "\n", - "**③ Connect VS Code** (Remote-SSH to the login node):\n", + "### Part B (continued). Connect VS Code (Remote-SSH to the login node)\n", "\n", - "1. **Select Kernel → Select Another Kernel… → Existing Jupyter Server**, paste `http://.hostmgmt.cm.aurora.alcf.anl.gov:8888/?token=`. Use the **full** hostname: it matches `no_proxy=*.alcf.anl.gov`, so the request isn't routed through the ALCF proxy (`502 Bad Gateway`).\n", + "1. Open this notebook, then **Select Kernel → Select Another Kernel… → Existing Jupyter Server**, and paste `http://.hostmgmt.cm.aurora.alcf.anl.gov:8888/?token=`. Use the **full** hostname: it matches `no_proxy=*.alcf.anl.gov`, so the request isn't routed through the ALCF proxy (`502 Bad Gateway`).\n", "2. Pick **PRISM BioReason (Aurora)**, not \"Python 3\".\n", "3. If it hangs on \"Connecting to kernel\", set `\"http.proxySupport\": \"off\"` in **Remote Settings** and run **Developer: Reload Window**. `403 Forbidden` means a stale token: each server start makes a new one unless you use a fixed token.\n", "\n", - "**④ Optional site paths.** The README's `PRISM_*` variables (`cp .env.template .env`, Aurora values in `.env.aurora.example`) are **not** needed here: section 1 sets everything this notebook and its jobs use.\n", + "### Optional site paths\n", "\n", - "> **Why not `tools/build_aurora_env.sh`?** The README's canonical Aurora build pins `frameworks/2025.3.1` and its lockfile. The BioReason submit scripts and this tutorial run on **`frameworks/2026.1.0`**, the stack everything here was tested on, which is why they use their own venv. Don't mix the two: a venv built against one frameworks module and loaded under the other fails with import or ABI errors. Note that 2026.1.0 ships transformers 5.x, so only Nucleotide Transformer **v1** checkpoints (e.g. the 2.5B multi-species one used here) load. NT-v2's remote code doesn't.\n", + "The README's `PRISM_*` variables (`cp .env.template .env`, Aurora values in `.env.aurora.example`) are **not** needed here: section 1 sets everything this notebook and its jobs use from `PROJECT`.\n", "\n", - "Section 2 below verifies the result (12 XPU tiles, venv active, no missing packages)." + "Section 2 below verifies the result (12 XPU tiles, venv active, no missing packages).\n" ] }, { @@ -156,12 +177,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "repo /home/abalaji/projects/modcon/release/BaseMM_PRISM\n", + "repo ${REPO}\n", "run tag demo-20261011-2324\n", - "HF_HOME /flare/ModCon/abalaji/cache/model_weights\n", - "PRISM_DATA_ROOT /lus/flare/projects/ModCon/abalaji/cache\n", - "PRISM_CKPT_ROOT /lus/flare/projects/ModCon/abalaji/modcon/checkpoints\n", - "PRISM_OUTPUT_ROOT /lus/flare/projects/ModCon/abalaji/output\n" + "HF_HOME ${HF_HOME}\n", + "PRISM_DATA_ROOT ${PRISM_DATA_ROOT}\n", + "PRISM_CKPT_ROOT ${PRISM_CKPT_ROOT}\n", + "PRISM_OUTPUT_ROOT ${PRISM_OUTPUT_ROOT}\n" ] } ], @@ -170,14 +191,18 @@ "from pathlib import Path\n", "\n", "USER = getpass.getuser()\n", - "PROJECT = \"ModCon\" # PBS allocation (-A)\n", + "PROJECT = os.environ.get(\"PROJECT\", \"\") # PBS allocation (-A) and project directory name; export PROJECT=\n", + "assert PROJECT != \"\", \"set the PROJECT environment variable to your PBS allocation before starting Jupyter\"\n", "\n", "REPO = Path.cwd().resolve().parents[1] if Path.cwd().name == \"bioreason\" else Path.cwd().resolve()\n", - "PRISM_DATA_ROOT = f\"/lus/flare/projects/{PROJECT}/{USER}/cache\"\n", - "PRISM_OUTPUT_ROOT = f\"/lus/flare/projects/{PROJECT}/{USER}/output\"\n", - "PRISM_CKPT_ROOT = f\"/lus/flare/projects/{PROJECT}/{USER}/modcon/checkpoints\" # = the submit scripts' default\n", - "HF_HOME = f\"/flare/{PROJECT}/{USER}/cache/model_weights\" # = the submit scripts' HF_HOME\n", - "PRISM_VENV = os.environ.get(\"PRISM_VENV\", f\"/flare/{PROJECT}/{USER}/prism-envs/bioreason-fw2026.1\")\n", + "# Aurora layout, built from parts; each root can be overridden from the environment.\n", + "LUS = Path(\"/\", \"lus\", \"flare\", \"projects\", PROJECT, USER) # Lustre project space\n", + "FLARE = Path(\"/\", \"flare\", PROJECT, USER) # home-style project space\n", + "PRISM_DATA_ROOT = os.environ.get(\"PRISM_DATA_ROOT\", str(LUS / \"cache\"))\n", + "PRISM_OUTPUT_ROOT = os.environ.get(\"PRISM_OUTPUT_ROOT\", str(LUS / \"output\"))\n", + "PRISM_CKPT_ROOT = os.environ.get(\"PRISM_CKPT_ROOT\", str(LUS / \"modcon\" / \"checkpoints\")) # = the submit scripts' default\n", + "HF_HOME = os.environ.get(\"HF_HOME\", str(FLARE / \"cache\" / \"model_weights\")) # = the submit scripts' HF_HOME\n", + "PRISM_VENV = os.environ.get(\"PRISM_VENV\", str(FLARE / \"prism-envs\" / \"bioreason-fw2026.1\"))\n", "PROXY = \"http://proxy.alcf.anl.gov:3128\"\n", "\n", "RUN_TAG = datetime.datetime.now().strftime(\"demo-%Y%m%d-%H%M\") # one tag for all runs from this session\n", @@ -234,8 +259,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "python /flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1/bin/python\n", - "in venv True (sys.prefix=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1)\n", + "python ${PRISM_VENV_ROOT}/bioreason-fw2026.1/bin/python\n", + "in venv True (sys.prefix=${PRISM_VENV_ROOT}/bioreason-fw2026.1)\n", "torch 2.13.0a0+gitcf30153 | transformers 5.14.1\n", "XPU tiles 12 (expect 12 on an Aurora node)\n", "missing none\n" @@ -295,9 +320,9 @@ "output_type": "stream", "text": [ "llm: allenai/OLMo-1B-0724-hf\n", - " -> /flare/ModCon/abalaji/cache/model_weights/hub/models--allenai--OLMo-1B-0724-hf/snapshots/d7cbab742d80589e714b1a2d7f838dcd21cbe143\n", + " -> ${HF_HOME}/hub/models--allenai--OLMo-1B-0724-hf/snapshots/d7cbab742d80589e714b1a2d7f838dcd21cbe143\n", "dna: InstaDeepAI/nucleotide-transformer-2.5b-multi-species\n", - " -> /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n" + " -> ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n" ] } ], @@ -347,16 +372,16 @@ "output_type": "stream", "text": [ "find: warning: you have specified the global option -maxdepth after the argument -name, but global options are not positional, i.e., -maxdepth affects tests specified before it as well as those specified after it. Please specify global options before other arguments.\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg/data-00000-of-00001.arrow\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg/dataset_info.json\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards_test/kegg/data-00000-of-00001.arrow\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards_test/kegg/dataset_info.json\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards_val/kegg/data-00000-of-00001.arrow\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards_val/kegg/dataset_info.json\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_coding/data-00000-of-00001.arrow\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_coding/dataset_info.json\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_non_snv/data-00000-of-00001.arrow\n", - "/lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/variant_effect_non_snv/dataset_info.json\n" + "${PRISM_DATA_ROOT}/dna_local_shards/kegg/data-00000-of-00001.arrow\n", + "${PRISM_DATA_ROOT}/dna_local_shards/kegg/dataset_info.json\n", + "${PRISM_DATA_ROOT}/dna_local_shards_test/kegg/data-00000-of-00001.arrow\n", + "${PRISM_DATA_ROOT}/dna_local_shards_test/kegg/dataset_info.json\n", + "${PRISM_DATA_ROOT}/dna_local_shards_val/kegg/data-00000-of-00001.arrow\n", + "${PRISM_DATA_ROOT}/dna_local_shards_val/kegg/dataset_info.json\n", + "${PRISM_DATA_ROOT}/dna_local_shards/variant_effect_coding/data-00000-of-00001.arrow\n", + "${PRISM_DATA_ROOT}/dna_local_shards/variant_effect_coding/dataset_info.json\n", + "${PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv/data-00000-of-00001.arrow\n", + "${PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv/dataset_info.json\n" ] } ], @@ -778,50 +803,22 @@ "| `local_path` / `preferred_source` | `local` loads the Arrow files from section 3. `${PRISM_DATA_ROOT}` is expanded from the environment. |\n", "| `hf_id` | Hub fallback, and the id that `training.bioreason_dataset` refers to. |\n", "| `required_keys` | Columns that must exist. A dataset missing one fails loudly at load time. |\n", - "| `skip` | `true` excludes the dataset. Training presets flip this per run instead of editing the JSON. |\n", + "| `skip` | `true` excludes the dataset. The shipped JSON enables only `dna_bioreason` (KEGG); every other dataset is `skip: true`. |\n", "\n", - "**Per-run overrides.** Each BioReason training preset composes `src/conf/data/bioreason_kegg_only.yaml`, which patches entries without touching the JSON:" + "**KEGG only.** Because `dna_bioreason` is the only entry with `skip: false`, all three BioReason presets (`training=bioreason_projector|sft|grpo`) train on KEGG without any data override. The cell below lists the zone-A datasets that are switched on:" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "cell-020", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "`src/conf/data/bioreason_kegg_only.yaml:1-16`\n", - "```yaml\n", - " 1 # BioReason KEGG-only data mix (wanglab/kegg via the dna_bioreason handler).\n", - " 2 # Pins every zone_a dataset that is default-on in datasets_config.json off, so\n", - " 3 # the SFT run cannot silently mix in another stream (ts_qa is skip=false there).\n", - " 4 #\n", - " 5 # Composed automatically by training=bioreason_{projector,sft,grpo} via their\n", - " 6 # defaults list (/data@_global_.data) -- do not also pass +data=bioreason_kegg_only.\n", - " 7 dataset_overrides:\n", - " 8 dna_bioreason:\n", - " 9 skip: false\n", - " 10 preferred_source: local\n", - " 11 dna_bioreason_variant_effect_coding:\n", - " 12 skip: true\n", - " 13 dna_bioreason_variant_effect_non_snv:\n", - " 14 skip: true\n", - " 15 ts_qa:\n", - " 16 skip: true\n", - "```" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "H.show_source(\"src/conf/data/bioreason_kegg_only.yaml\")" + "import json\n", + "zone_a = json.load(open(REPO / \"src/data/datasets_config.json\"))[\"zone_a\"][\"datasets\"]\n", + "enabled = {k: v[\"hf_id\"] for k, v in zone_a.items() if not v.get(\"skip\")}\n", + "print(\"enabled zone-A datasets:\", enabled)\n" ] }, { @@ -831,9 +828,9 @@ "source": [ "### Adding data: a KEGG + variant-effect mix\n", "\n", - "The original BioReason recipe mixes KEGG with the variant-effect sets (weights 0.5 / 0.25 / 0.25). The variant-effect entries already exist in the JSON, so a **new data YAML** that turns them on and points them at our downloads is all it takes. Passing `data=bioreason_demo_mix` on the command line swaps it in for `bioreason_kegg_only`.\n", + "The original BioReason recipe mixes KEGG with the variant-effect sets (weights 0.5 / 0.25 / 0.25). The variant-effect entries already exist in the JSON, so a **new data YAML** that turns them on and points them at our downloads is all it takes. Passing `data=bioreason_demo_mix` on the command line applies it on top of the KEGG-only JSON.\n", "\n", - "Because it *replaces* `bioreason_kegg_only`, it has to pin every default-on stream itself (`ts_qa`). `${oc.env:PRISM_DATA_ROOT}` is resolved when the job starts." + "`${oc.env:PRISM_DATA_ROOT}` is resolved when the job starts." ] }, { @@ -889,8 +886,6 @@ " preferred_source: local\n", " weight: 0.25\n", " local_path: ${oc.env:PRISM_DATA_ROOT}/dna_local_shards/variant_effect_non_snv\n", - " ts_qa:\n", - " skip: true\n", "''')\n", "print(MIX_YAML.read_text())" ] @@ -955,21 +950,21 @@ " \n", " dna_bioreason\n", " 0.50\n", - " /lus/flare/projects/ModCon/abalaji/cache/dna_l...\n", + " ${PRISM_DATA_ROOT}/dna_l...\n", " True\n", " []\n", " \n", " \n", " dna_bioreason_variant_effect_coding\n", " 0.25\n", - " /lus/flare/projects/ModCon/abalaji/cache/dna_l...\n", + " ${PRISM_DATA_ROOT}/dna_l...\n", " True\n", " []\n", " \n", " \n", " dna_bioreason_variant_effect_non_snv\n", " 0.25\n", - " /lus/flare/projects/ModCon/abalaji/cache/dna_l...\n", + " ${PRISM_DATA_ROOT}/dna_l...\n", " True\n", " []\n", " \n", @@ -986,9 +981,9 @@ "\n", " local_path \\\n", "dataset \n", - "dna_bioreason /lus/flare/projects/ModCon/abalaji/cache/dna_l... \n", - "dna_bioreason_variant_effect_coding /lus/flare/projects/ModCon/abalaji/cache/dna_l... \n", - "dna_bioreason_variant_effect_non_snv /lus/flare/projects/ModCon/abalaji/cache/dna_l... \n", + "dna_bioreason ${PRISM_DATA_ROOT}/dna_l... \n", + "dna_bioreason_variant_effect_coding ${PRISM_DATA_ROOT}/dna_l... \n", + "dna_bioreason_variant_effect_non_snv ${PRISM_DATA_ROOT}/dna_l... \n", "\n", " arrow found missing required_keys \n", "dataset \n", @@ -1397,7 +1392,7 @@ "output_type": "stream", "text": [ "backbone allenai/OLMo-1B-0724-hf\n", - "dna_model_name /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "dna_model_name ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", "d_dna 768 | max_dna_length 1024 | modalities ['text', 'dna']\n" ] } @@ -1423,7 +1418,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "2026-10-11 23:25:27,060 - src.encoders.dna - INFO - Loading DNA Encoder: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" + "2026-10-11 23:25:27,060 - src.encoders.dna - INFO - Loading DNA Encoder: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" ] }, { @@ -1446,7 +1441,7 @@ "text": [ "2026-10-11 23:26:08,406 - src.encoders.dna - INFO - DNA model hidden_states[-1] dim (probed): 2560\n", "2026-10-11 23:26:08,407 - src.encoders.dna - INFO - DNA Encoder projection: 2560 -> 768\n", - "2026-10-11 23:26:08,415 - src.encoders.dna - INFO - DNA Encoder loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n" + "2026-10-11 23:26:08,415 - src.encoders.dna - INFO - DNA Encoder loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n" ] }, { @@ -1754,7 +1749,7 @@ "2026-10-11 23:26:22,439 - src.model - INFO - Freezing Backbone weights...\n", "2026-10-11 23:26:22,440 - src.model - INFO - Backbone hidden dim detected: 2048\n", "2026-10-11 23:26:22,496 - src.model - INFO - Projector config: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm, embed_scale=0.0055, hidden_mult=1, num_layers=2\n", - "2026-10-11 23:26:22,496 - src.encoders.dna - INFO - Loading DNA Encoder: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" + "2026-10-11 23:26:22,496 - src.encoders.dna - INFO - Loading DNA Encoder: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" ] }, { @@ -1777,7 +1772,7 @@ "text": [ "2026-10-11 23:26:25,220 - src.encoders.dna - INFO - DNA model hidden_states[-1] dim (probed): 2560\n", "2026-10-11 23:26:25,221 - src.encoders.dna - INFO - DNA Encoder projection: 2560 -> 768\n", - "2026-10-11 23:26:25,227 - src.encoders.dna - INFO - DNA Encoder loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", + "2026-10-11 23:26:25,227 - src.encoders.dna - INFO - DNA Encoder loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", "2026-10-11 23:26:25,266 - src.modules.projector - INFO - Modality embedding: scale=0.0055, expected_norm=0.2489\n", "2026-10-11 23:26:25,266 - src.modules.projector - INFO - ModalityProjector initialized: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm\n", "2026-10-11 23:26:25,267 - src.model - INFO - Freezing Modality Encoders...\n" @@ -1990,7 +1985,7 @@ "2026-10-11 23:26:27,511 - src.model - INFO - Freezing Backbone weights...\n", "2026-10-11 23:26:27,513 - src.model - INFO - Backbone hidden dim detected: 2048\n", "2026-10-11 23:26:27,569 - src.model - INFO - Projector config: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm, embed_scale=0.0055, hidden_mult=2, num_layers=3\n", - "2026-10-11 23:26:27,570 - src.encoders.dna - INFO - Loading DNA Encoder: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" + "2026-10-11 23:26:27,570 - src.encoders.dna - INFO - Loading DNA Encoder: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" ] }, { @@ -2013,7 +2008,7 @@ "text": [ "2026-10-11 23:26:30,280 - src.encoders.dna - INFO - DNA model hidden_states[-1] dim (probed): 2560\n", "2026-10-11 23:26:30,281 - src.encoders.dna - INFO - DNA Encoder projection: 2560 -> 768\n", - "2026-10-11 23:26:30,288 - src.encoders.dna - INFO - DNA Encoder loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", + "2026-10-11 23:26:30,288 - src.encoders.dna - INFO - DNA Encoder loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", "2026-10-11 23:26:30,490 - src.modules.projector - INFO - Modality embedding: scale=0.0055, expected_norm=0.2489\n", "2026-10-11 23:26:30,491 - src.modules.projector - INFO - ModalityProjector initialized: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm\n", "2026-10-11 23:26:30,491 - src.model - INFO - Freezing Modality Encoders...\n", @@ -2293,9 +2288,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "[INFO] DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", - "[INFO] DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", - "2026-10-11 23:26:37,120 - src.data.multimodal - INFO - DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "[INFO] DNA tokenizer loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "[INFO] DNA tokenizer loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "2026-10-11 23:26:37,120 - src.data.multimodal - INFO - DNA tokenizer loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", "[INFO] dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", "[INFO] dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", "2026-10-11 23:26:37,123 - src.data.multimodal - INFO - dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", @@ -2387,9 +2382,9 @@ "[INFO] [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", "[INFO] [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", "2026-10-11 23:26:37,491 - src.data.multimodal - INFO - [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", - "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n", - "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n", - "2026-10-11 23:26:37,497 - src.data.multimodal - INFO - [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n" + "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: ${PRISM_DATA_ROOT}/dna_local_shards/kegg (1 shards)\n", + "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: ${PRISM_DATA_ROOT}/dna_local_shards/kegg (1 shards)\n", + "2026-10-11 23:26:37,497 - src.data.multimodal - INFO - [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: ${PRISM_DATA_ROOT}/dna_local_shards/kegg (1 shards)\n" ] }, { @@ -2594,19 +2589,19 @@ "text": [ "Step 0: Loss 9.5068\n", "Training Zone A: 50%|█████ | 10/20 [00:08<00:05, 1.87it/s, loss=9.42]Step 10: Loss 9.4199\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[Step final] Saving Checkpoint to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", - "[2026-10-11 23:28:20,589][accelerate.accelerator][INFO] - [RANK 0] Saving current state to /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final\n" + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[Step final] Saving Checkpoint to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final...\n", + "[2026-10-11 23:28:20,589][accelerate.accelerator][INFO] - [RANK 0] Saving current state to ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final\n" ] } ], @@ -2683,11 +2678,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-PROJECTOR-KEGG/demo-20261011-2324,MAX_STEPS=100,SAVE_EVERY=50 -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/projector.log /lus/flare/projects/ModCon/abalaji/output/demo_scripts/demo-20261011-2324/projector.sh\n", - "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-SFT-KEGG/demo-20261011-2324,STAGE1_CKPT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-PROJECTOR-KEGG/demo-20261011-2324/checkpoints/step_final,MAX_STEPS=500 -W depend=afterok:DRYRUN -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/sft.log -l walltime=02:00:00 /lus/flare/projects/ModCon/abalaji/output/demo_scripts/demo-20261011-2324/sft.sh\n", - "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-GRPO-KEGG/demo-20261011-2324,STAGE1_CKPT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,MAX_STEPS=100,SAVE_EVERY=50 -W depend=afterok:DRYRUN -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/grpo.log /home/abalaji/projects/modcon/release/BaseMM_PRISM/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh\n", - "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,CKPT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,OUTPUT=/lus/flare/projects/ModCon/abalaji/output/BIOREASON-EVAL-KEGG/demo-20261011-2324/sft.json -W depend=afterok:DRYRUN -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/eval-sft.log /home/abalaji/projects/modcon/release/BaseMM_PRISM/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh\n", - "qsub -v PRISM_DATA_ROOT=/lus/flare/projects/ModCon/abalaji/cache,PRISM_OUTPUT_ROOT=/lus/flare/projects/ModCon/abalaji/output,PRISM_CKPT_ROOT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints,PRISM_VENV=/flare/ModCon/abalaji/prism-envs/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,CKPT=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,GRPO_ADAPTER=/lus/flare/projects/ModCon/abalaji/modcon/checkpoints/BIOREASON-GRPO-KEGG/demo-20261011-2324/checkpoints/step_final/lora_adapter.pt,OUTPUT=/lus/flare/projects/ModCon/abalaji/output/BIOREASON-EVAL-KEGG/demo-20261011-2324/sft+grpo.json -W depend=afterok:DRYRUN -o /lus/flare/projects/ModCon/abalaji/output/demo_logs/demo-20261011-2324/eval-sft+grpo.log /home/abalaji/projects/modcon/release/BaseMM_PRISM/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh\n" + "qsub -v PRISM_DATA_ROOT=${PRISM_DATA_ROOT},PRISM_OUTPUT_ROOT=${PRISM_OUTPUT_ROOT},PRISM_CKPT_ROOT=${PRISM_CKPT_ROOT},PRISM_VENV=${PRISM_VENV_ROOT}/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=${PRISM_CKPT_ROOT}/BIOREASON-PROJECTOR-KEGG/demo-20261011-2324,MAX_STEPS=100,SAVE_EVERY=50 -o ${PRISM_OUTPUT_ROOT}/demo_logs/demo-20261011-2324/projector.log ${PRISM_OUTPUT_ROOT}/demo_scripts/demo-20261011-2324/projector.sh\n", + "qsub -v PRISM_DATA_ROOT=${PRISM_DATA_ROOT},PRISM_OUTPUT_ROOT=${PRISM_OUTPUT_ROOT},PRISM_CKPT_ROOT=${PRISM_CKPT_ROOT},PRISM_VENV=${PRISM_VENV_ROOT}/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=${PRISM_CKPT_ROOT}/BIOREASON-SFT-KEGG/demo-20261011-2324,STAGE1_CKPT=${PRISM_CKPT_ROOT}/BIOREASON-PROJECTOR-KEGG/demo-20261011-2324/checkpoints/step_final,MAX_STEPS=500 -W depend=afterok:DRYRUN -o ${PRISM_OUTPUT_ROOT}/demo_logs/demo-20261011-2324/sft.log -l walltime=02:00:00 ${PRISM_OUTPUT_ROOT}/demo_scripts/demo-20261011-2324/sft.sh\n", + "qsub -v PRISM_DATA_ROOT=${PRISM_DATA_ROOT},PRISM_OUTPUT_ROOT=${PRISM_OUTPUT_ROOT},PRISM_CKPT_ROOT=${PRISM_CKPT_ROOT},PRISM_VENV=${PRISM_VENV_ROOT}/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,HYDRA_RUN_DIR=${PRISM_CKPT_ROOT}/BIOREASON-GRPO-KEGG/demo-20261011-2324,STAGE1_CKPT=${PRISM_CKPT_ROOT}/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,MAX_STEPS=100,SAVE_EVERY=50 -W depend=afterok:DRYRUN -o ${PRISM_OUTPUT_ROOT}/demo_logs/demo-20261011-2324/grpo.log ${REPO}/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh\n", + "qsub -v PRISM_DATA_ROOT=${PRISM_DATA_ROOT},PRISM_OUTPUT_ROOT=${PRISM_OUTPUT_ROOT},PRISM_CKPT_ROOT=${PRISM_CKPT_ROOT},PRISM_VENV=${PRISM_VENV_ROOT}/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,CKPT=${PRISM_CKPT_ROOT}/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,OUTPUT=${PRISM_OUTPUT_ROOT}/BIOREASON-EVAL-KEGG/demo-20261011-2324/sft.json -W depend=afterok:DRYRUN -o ${PRISM_OUTPUT_ROOT}/demo_logs/demo-20261011-2324/eval-sft.log ${REPO}/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh\n", + "qsub -v PRISM_DATA_ROOT=${PRISM_DATA_ROOT},PRISM_OUTPUT_ROOT=${PRISM_OUTPUT_ROOT},PRISM_CKPT_ROOT=${PRISM_CKPT_ROOT},PRISM_VENV=${PRISM_VENV_ROOT}/bioreason-fw2026.1,WANDB_MODE=offline,MAX_SEQ_LENGTH=4096,RUN_TAG=demo-20261011-2324,CKPT=${PRISM_CKPT_ROOT}/BIOREASON-SFT-KEGG/demo-20261011-2324/checkpoints/step_final,GRPO_ADAPTER=${PRISM_CKPT_ROOT}/BIOREASON-GRPO-KEGG/demo-20261011-2324/checkpoints/step_final/lora_adapter.pt,OUTPUT=${PRISM_OUTPUT_ROOT}/BIOREASON-EVAL-KEGG/demo-20261011-2324/sft+grpo.json -W depend=afterok:DRYRUN -o ${PRISM_OUTPUT_ROOT}/demo_logs/demo-20261011-2324/eval-sft+grpo.log ${REPO}/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh\n" ] }, { @@ -2718,7 +2713,7 @@ "common = {k: ENV[k] for k in (\"PRISM_DATA_ROOT\", \"PRISM_OUTPUT_ROOT\", \"PRISM_CKPT_ROOT\", \"PRISM_VENV\", \"WANDB_MODE\", \"MAX_SEQ_LENGTH\")} | {\"RUN_TAG\": RUN_TAG}\n", "\n", "JOBS = {}\n", - "q = lambda *a, **k: H.qsub(*a, cwd=str(REPO), dry_run=not SUBMIT, **k)\n", + "q = lambda *a, **k: H.qsub(*a, cwd=str(REPO), dry_run=not SUBMIT, account=PROJECT, **k)\n", "JOBS[\"projector\"] = q(PROJ_SH, {**common, \"HYDRA_RUN_DIR\": RUN[\"projector\"], \"MAX_STEPS\": 100, \"SAVE_EVERY\": 50},\n", " log=LOGS[\"projector\"])\n", "JOBS[\"sft\"] = q(SFT_SH, {**common, \"HYDRA_RUN_DIR\": RUN[\"sft\"], \"STAGE1_CKPT\": CKPT_PROJ, \"MAX_STEPS\": 500},\n", @@ -2832,7 +2827,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "checkpoint: /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final\n" + "checkpoint: ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final\n" ] }, { @@ -2856,7 +2851,7 @@ "2026-10-11 23:28:36,130 - src.model - INFO - Freezing Backbone weights...\n", "2026-10-11 23:28:36,131 - src.model - INFO - Backbone hidden dim detected: 2048\n", "2026-10-11 23:28:36,187 - src.model - INFO - Projector config: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm, embed_scale=0.0055, hidden_mult=1, num_layers=2\n", - "2026-10-11 23:28:36,188 - src.encoders.dna - INFO - Loading DNA Encoder: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" + "2026-10-11 23:28:36,188 - src.encoders.dna - INFO - Loading DNA Encoder: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e...\n" ] }, { @@ -2879,7 +2874,7 @@ "text": [ "2026-10-11 23:28:38,919 - src.encoders.dna - INFO - DNA model hidden_states[-1] dim (probed): 2560\n", "2026-10-11 23:28:38,920 - src.encoders.dna - INFO - DNA Encoder projection: 2560 -> 768\n", - "2026-10-11 23:28:38,927 - src.encoders.dna - INFO - DNA Encoder loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", + "2026-10-11 23:28:38,927 - src.encoders.dna - INFO - DNA Encoder loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e (model_hidden=2560, output=768, tokens_per_instance=1024)\n", "2026-10-11 23:28:38,969 - src.modules.projector - INFO - Modality embedding: scale=0.0055, expected_norm=0.2489\n", "2026-10-11 23:28:38,969 - src.modules.projector - INFO - ModalityProjector initialized: norm_mode=l2_token, target_norm=0.25, embed_pos=after_norm\n", "2026-10-11 23:28:38,970 - src.model - INFO - Freezing Modality Encoders...\n" @@ -2889,7 +2884,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[Resume] Loading model weights from /lus/flare/projects/ModCon/abalaji/modcon/checkpoints/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final/model.safetensors\n", + "[Resume] Loading model weights from ${PRISM_CKPT_ROOT}/DEMO-SMOKE-PROJECTOR/demo-20261011-2324/checkpoints/step_final/model.safetensors\n", "[Resume] Loaded 646 weight tensors successfully\n", "[Resume] Checkpoint was saved at step final\n" ] @@ -2898,9 +2893,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "[INFO] DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", - "[INFO] DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", - "2026-10-11 23:28:53,483 - src.data.multimodal - INFO - DNA tokenizer loaded: /flare/ModCon/abalaji/cache/model_weights/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "[INFO] DNA tokenizer loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "[INFO] DNA tokenizer loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", + "2026-10-11 23:28:53,483 - src.data.multimodal - INFO - DNA tokenizer loaded: ${HF_HOME}/hub/models--InstaDeepAI--nucleotide-transformer-2.5b-multi-species/snapshots/b746b125aacd1b0970c05a32fd71ba726754542e\n", "[INFO] dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", "[INFO] dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", "2026-10-11 23:28:53,485 - src.data.multimodal - INFO - dataset_overrides applied: dna_bioreason <- {'skip': False, 'preferred_source': 'local'}\n", @@ -2992,9 +2987,9 @@ "[INFO] [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", "[INFO] [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", "2026-10-11 23:28:53,701 - src.data.multimodal - INFO - [DATA CONFIG] Dataset: DNA sequences | Priority: LOCAL\n", - "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n", - "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n", - "2026-10-11 23:28:53,704 - src.data.multimodal - INFO - [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: /lus/flare/projects/ModCon/abalaji/cache/dna_local_shards/kegg (1 shards)\n" + "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: ${PRISM_DATA_ROOT}/dna_local_shards/kegg (1 shards)\n", + "[INFO] [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: ${PRISM_DATA_ROOT}/dna_local_shards/kegg (1 shards)\n", + "2026-10-11 23:28:53,704 - src.data.multimodal - INFO - [DATA LOAD] Source: LOCAL (Raw Arrow) | Path: ${PRISM_DATA_ROOT}/dna_local_shards/kegg (1 shards)\n" ] }, { diff --git a/applications/bioreason/helpers/demo_helpers.py b/applications/bioreason/helpers/demo_helpers.py index 3f5ef45..3ce7776 100644 --- a/applications/bioreason/helpers/demo_helpers.py +++ b/applications/bioreason/helpers/demo_helpers.py @@ -370,9 +370,11 @@ def patch_stage_script(src: str | Path, dst: str | Path) -> Path: def qsub(script, env: dict | None = None, depend: str | None = None, log: str | None = None, queue: str | None = None, walltime: str | None = None, name: str | None = None, - cwd: str | None = None, dry_run: bool = False) -> str: - """Submit ``script`` and return the job id. ``env`` is passed with -v.""" + cwd: str | None = None, dry_run: bool = False, account: str | None = None) -> str: + """Submit ``script`` and return the job id. ``env`` is passed with -v; ``account`` with -A.""" cmd = ["qsub"] + if account: + cmd += ["-A", account] if env: bad = [k for k, v in env.items() if "," in str(v)] if bad: diff --git a/applications/bioreason/helpers/kernel_wrapper.sh b/applications/bioreason/helpers/kernel_wrapper.sh index 2cc5adb..ec3a6f5 100755 --- a/applications/bioreason/helpers/kernel_wrapper.sh +++ b/applications/bioreason/helpers/kernel_wrapper.sh @@ -10,7 +10,7 @@ # Do not call directly; Jupyter passes `-f ` as "$@". FRAMEWORKS_MODULE="${FRAMEWORKS_MODULE:-frameworks/2026.1.0}" -PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" module use /soft/modulefiles module load "${FRAMEWORKS_MODULE}" diff --git a/applications/bioreason/helpers/setup_jupyter_kernel.sh b/applications/bioreason/helpers/setup_jupyter_kernel.sh index 6c25ee5..3ff3f2c 100755 --- a/applications/bioreason/helpers/setup_jupyter_kernel.sh +++ b/applications/bioreason/helpers/setup_jupyter_kernel.sh @@ -1,15 +1,14 @@ #!/bin/bash -l # One-time setup for applications/bioreason/bioreason_demo.ipynb on Aurora. # -# 1. Builds (or reuses) the BioReason venv on top of frameworks/2026.1.0 -- -# the same venv the scripts/training/submit_bioreason_*.sh scripts activate. -# 2. Installs the notebook-only extras (ipykernel, ipywidgets) plus the -# training extras the frameworks module lacks. -# 3. Registers a Jupyter kernel `prism-bioreason` whose argv is -# kernel_wrapper.sh, so the kernel always starts with the module + venv -# loaded (VS Code Remote and jupyter lab both pick it up). +# Installs Jupyter into the PRISM venv and registers a kernel `prism-bioreason` whose argv is +# kernel_wrapper.sh, so the kernel always starts with the module + venv loaded (VS Code +# Remote and jupyter lab both pick it up). Jupyter is not part of PRISM's requirements. +# +# Requires the venv from helpers/setup_prism_env.sh (run that first). # # Run once from a login node (UAN; it has direct internet access): +# export PRISM_VENV=/flare//$USER/prism-envs/bioreason-fw2026.1 # bash applications/bioreason/helpers/setup_jupyter_kernel.sh # Then, from VS Code connected to the interactive compute node, open the # notebook and pick the "PRISM BioReason (Aurora)" kernel. @@ -17,34 +16,26 @@ set -eo pipefail HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PRISM_DIR="$( cd "${HERE}/../../.." && pwd )" FRAMEWORKS_MODULE="${FRAMEWORKS_MODULE:-frameworks/2026.1.0}" -PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" KERNEL_NAME="prism-bioreason" +case "${PRISM_VENV}" in *""*) echo "ERROR: set PRISM_VENV to your venv path, e.g. /flare/\$PROJECT/\$USER/prism-envs/bioreason-fw2026.1"; exit 1;; esac + module use /soft/modulefiles module load "${FRAMEWORKS_MODULE}" -if ! command -v uv >/dev/null 2>&1; then - echo "ERROR: uv is not on PATH. Install: curl -LsSf https://astral.sh/uv/install.sh | sh" - exit 1 -fi - if [ ! -f "${PRISM_VENV}/bin/activate" ]; then - echo "Creating ${PRISM_VENV} (--system-site-packages over ${FRAMEWORKS_MODULE})" - uv venv --system-site-packages -p "$(which python)" "${PRISM_VENV}" + echo "ERROR: ${PRISM_VENV} not found -- run helpers/setup_prism_env.sh first." + exit 1 fi source "${PRISM_VENV}/bin/activate" +command -v uv >/dev/null 2>&1 || { echo "ERROR: uv is not on PATH (see setup_prism_env.sh)"; exit 1; } -# Same extras as the recipe in submit_bioreason_sft.sh; --no-deps keeps the -# module's XPU torch from being replaced by a PyPI/CUDA wheel. -uv pip install wandb braceexpand==0.1.7 -uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 # jupyter_server: fallback when VS Code doesn't list the kernel -- run # `jupyter server --no-browser --ip=$(hostname) --port 8888` on the compute node # and connect via "Select Kernel -> Existing Jupyter Server". uv pip install ipykernel ipywidgets jupyter_server -uv pip install --no-deps -e "${PRISM_DIR}" chmod +x "${HERE}/kernel_wrapper.sh" KERNEL_DIR="${HOME}/.local/share/jupyter/kernels/${KERNEL_NAME}" diff --git a/applications/bioreason/helpers/setup_prism_env.sh b/applications/bioreason/helpers/setup_prism_env.sh new file mode 100755 index 0000000..d8e101f --- /dev/null +++ b/applications/bioreason/helpers/setup_prism_env.sh @@ -0,0 +1,43 @@ +#!/bin/bash -l +# One-time PRISM environment for the BioReason application on Aurora (no Jupyter here). +# +# Builds (or reuses) the BioReason venv on top of frameworks/2026.1.0 -- the same venv the +# scripts/training/submit_bioreason_*.sh scripts activate -- and installs the training extras +# the frameworks module lacks plus PRISM itself (--no-deps, so the module's XPU torch stays). +# +# Run once from a login node (UAN; it has direct internet access): +# export PRISM_VENV=/flare//$USER/prism-envs/bioreason-fw2026.1 +# bash applications/bioreason/helpers/setup_prism_env.sh +# For the notebook, then run helpers/setup_jupyter_kernel.sh. + +set -eo pipefail + +HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PRISM_DIR="$( cd "${HERE}/../../.." && pwd )" +FRAMEWORKS_MODULE="${FRAMEWORKS_MODULE:-frameworks/2026.1.0}" +PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" +case "${PRISM_VENV}" in *""*) echo "ERROR: set PRISM_VENV to your venv path, e.g. /flare/\$PROJECT/\$USER/prism-envs/bioreason-fw2026.1"; exit 1;; esac + +module use /soft/modulefiles +module load "${FRAMEWORKS_MODULE}" + +if ! command -v uv >/dev/null 2>&1; then + echo "ERROR: uv is not on PATH. Install: curl -LsSf https://astral.sh/uv/install.sh | sh" + exit 1 +fi + +if [ ! -f "${PRISM_VENV}/bin/activate" ]; then + echo "Creating ${PRISM_VENV} (--system-site-packages over ${FRAMEWORKS_MODULE})" + uv venv --system-site-packages -p "$(which python)" "${PRISM_VENV}" +fi +source "${PRISM_VENV}/bin/activate" + +# Same extras as the recipe in submit_bioreason_sft.sh; --no-deps keeps the +# module's XPU torch from being replaced by a PyPI/CUDA wheel. +uv pip install wandb braceexpand==0.1.7 +uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 +uv pip install --no-deps -e "${PRISM_DIR}" + +echo "PRISM environment ready:" +echo " venv: ${PRISM_VENV}" +echo " module: ${FRAMEWORKS_MODULE}" diff --git a/applications/bioreason/scripts/eval/submit_eval_kegg.sh b/applications/bioreason/scripts/eval/submit_eval_kegg.sh index 49fc462..249d6b9 100755 --- a/applications/bioreason/scripts/eval/submit_eval_kegg.sh +++ b/applications/bioreason/scripts/eval/submit_eval_kegg.sh @@ -9,7 +9,11 @@ set -euo pipefail -PROJ_DIR="/home/abalaji/projects/modcon/genome/BaseMM_PRISM" +PROJ_DIR="${PROJ_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." 2>/dev/null && pwd)}" +# Under qsub the script runs from a spool copy, so BASH_SOURCE points there. +if [ -n "${PBS_O_WORKDIR:-}" ] && [ ! -f "${PROJ_DIR}/src/train.py" ]; then + PROJ_DIR="${PBS_O_WORKDIR}" +fi CHECKPOINT_DIR="/lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output/GENOME-SFT-1B/2026-06-29/07-41-10/checkpoints/step_8000" # SFT with sample weigting and answer structure : /lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output/GENOME-SFT-1B/2026-06-29/07-41-10/checkpoints/step_3000 # Set to "true" to run interleaved mode, "false" for prefix mode. diff --git a/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh b/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh index fe68aa9..e25dc8e 100755 --- a/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh +++ b/applications/bioreason/scripts/eval/submit_eval_kegg_aurora.sh @@ -4,7 +4,7 @@ #PBS -l walltime=01:00:00 #PBS -l filesystems=home:flare #PBS -q debug -#PBS -A ModCon +# Allocation is not hardcoded: submit with `qsub -A ...`. #PBS -k doe #PBS -j oe # @@ -36,8 +36,8 @@ MAX_NEW_TOKENS="${MAX_NEW_TOKENS:-1024}" DNA_MODE="${DNA_MODE:-real}" case "${DNA_MODE}" in real|shuffled|blank) ;; *) echo "ERROR: DNA_MODE must be real|shuffled|blank"; exit 1;; esac -export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects/ModCon/${USER}/cache}" -export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/${USER}/output}" +export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects//${USER}/cache}" +export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects//${USER}/output}" RESULT_NAME="results.json"; [ "${DNA_MODE}" != "real" ] && RESULT_NAME="results_${DNA_MODE}.json" OUTPUT="${OUTPUT:-${PRISM_OUTPUT_ROOT}/BIOREASON-EVAL-KEGG/${RUN_TAG}/${RESULT_NAME}}" DATA="${DATA:-${PRISM_DATA_ROOT}/dna_local_shards_${SPLIT}/kegg}" @@ -49,13 +49,13 @@ fi cd "${PROJ_DIR}" -export PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +export PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" module use /soft/modulefiles module load frameworks/2026.1.0 module load hdf5 source "${PRISM_VENV}/bin/activate" -export HF_HOME="${HF_HOME:-/flare/ModCon/${USER}/cache/model_weights}" +export HF_HOME="${HF_HOME:-/flare//${USER}/cache/model_weights}" export HF_HUB_OFFLINE=1 export TRANSFORMERS_OFFLINE=1 export HF_DATASETS_OFFLINE=1 diff --git a/applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh b/applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh index da38efd..9907a0e 100755 --- a/applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh +++ b/applications/bioreason/scripts/eval/submit_eval_sft_sweep.sh @@ -17,7 +17,7 @@ CKPT_DIR="${1:?usage: $0 /checkpoints}" SPLIT="${SPLIT:-val}" WALLTIME="${WALLTIME:-01:00:00}" TAG="${TAG:-sweep-$(date +%Y%m%d-%H%M%S)}" -PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/${USER}/output}" +PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects//${USER}/output}" OUT_DIR="${PRISM_OUTPUT_ROOT}/BIOREASON-EVAL-KEGG/${TAG}" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/applications/bioreason/scripts/test/verify_dna_dataset_mix.sh b/applications/bioreason/scripts/test/verify_dna_dataset_mix.sh index 5153924..f96db59 100644 --- a/applications/bioreason/scripts/test/verify_dna_dataset_mix.sh +++ b/applications/bioreason/scripts/test/verify_dna_dataset_mix.sh @@ -23,7 +23,11 @@ set -euo pipefail -PROJ_DIR="/home/abalaji/projects/modcon/genome/BaseMM_PRISM" +PROJ_DIR="${PROJ_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." 2>/dev/null && pwd)}" +# Under qsub the script runs from a spool copy, so BASH_SOURCE points there. +if [ -n "${PBS_O_WORKDIR:-}" ] && [ ! -f "${PROJ_DIR}/src/train.py" ]; then + PROJ_DIR="${PBS_O_WORKDIR}" +fi STAGE1_CKPT="/lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output/GENOME-SFT-1B/2026-06-29/07-41-10/checkpoints/step_8000" cd "${PROJ_DIR}" diff --git a/applications/bioreason/scripts/training/submit_bioreason_grpo.sh b/applications/bioreason/scripts/training/submit_bioreason_grpo.sh index 47690fe..884f0b1 100755 --- a/applications/bioreason/scripts/training/submit_bioreason_grpo.sh +++ b/applications/bioreason/scripts/training/submit_bioreason_grpo.sh @@ -11,7 +11,11 @@ set -euo pipefail -PROJ_DIR="/home/abalaji/projects/modcon/genome/BaseMM_PRISM" +PROJ_DIR="${PROJ_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." 2>/dev/null && pwd)}" +# Under qsub the script runs from a spool copy, so BASH_SOURCE points there. +if [ -n "${PBS_O_WORKDIR:-}" ] && [ ! -f "${PROJ_DIR}/src/train.py" ]; then + PROJ_DIR="${PBS_O_WORKDIR}" +fi OUTPUT_BASE="/lus/eagle/projects/argonne_tpc/abalaji/modcon/genome/output" RUN_ID="GENOME-GRPO" RUN_VARIANT="1B" diff --git a/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh b/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh index fe9ae10..d080496 100755 --- a/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh +++ b/applications/bioreason/scripts/training/submit_bioreason_grpo_aurora.sh @@ -4,7 +4,7 @@ #PBS -l walltime=04:00:00 #PBS -l filesystems=home:flare #PBS -q prod -#PBS -A ModCon +# Allocation is not hardcoded: submit with `qsub -A ...`. #PBS -k doe #PBS -j oe # @@ -41,15 +41,18 @@ fi RUN_ID="${RUN_ID:-BIOREASON-GRPO-KEGG}" RUN_VARIANT="1B" RUN_TAG="${RUN_TAG:-$(date +%Y%m%d-%H%M%S)}" -MAX_STEPS="${MAX_STEPS:-1000}" -NUM_GENERATIONS="${NUM_GENERATIONS:-8}" -MAX_COMPLETION="${MAX_COMPLETION:-800}" -SAVE_EVERY="${SAVE_EVERY:-100}" +# Hyperparameters come from src/conf/training/bioreason_grpo.yaml. Set any of +# MAX_STEPS / NUM_GENERATIONS / MAX_COMPLETION / SAVE_EVERY to override that value for one run. +TRAIN_OVERRIDES="" +[ -n "${MAX_STEPS:-}" ] && TRAIN_OVERRIDES+=" training.max_steps=${MAX_STEPS}" +[ -n "${NUM_GENERATIONS:-}" ] && TRAIN_OVERRIDES+=" training.grpo_num_generations=${NUM_GENERATIONS}" +[ -n "${MAX_COMPLETION:-}" ] && TRAIN_OVERRIDES+=" training.grpo_max_completion_length=${MAX_COMPLETION}" +[ -n "${SAVE_EVERY:-}" ] && TRAIN_OVERRIDES+=" training.save_every_n_steps=${SAVE_EVERY}" EXTRA_HYDRA="${EXTRA_HYDRA:-}" -export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects/ModCon/${USER}/cache}" -export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/${USER}/output}" -export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects/ModCon/${USER}/modcon/checkpoints}" +export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects//${USER}/cache}" +export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects//${USER}/output}" +export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects//${USER}/modcon/checkpoints}" HYDRA_RUN_DIR="${HYDRA_RUN_DIR:-${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG}}" # Stage 2 SFT checkpoint. Default: newest SFT step_final under PRISM_CKPT_ROOT. @@ -61,12 +64,12 @@ fi cd "${PROJ_DIR}" -export PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +export PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" module use /soft/modulefiles module load frameworks/2026.1.0 module load hdf5 if [ ! -f "${PRISM_VENV}/bin/activate" ]; then - echo "ERROR: PRISM_VENV not found: ${PRISM_VENV} (bash applications/bioreason/helpers/setup_jupyter_kernel.sh builds it)" + echo "ERROR: PRISM_VENV not found: ${PRISM_VENV} (bash applications/bioreason/helpers/setup_prism_env.sh builds it)" exit 1 fi source "${PRISM_VENV}/bin/activate" @@ -78,7 +81,7 @@ if [[ ! "${HOSTNAME}" =~ aurora-uan ]]; then export https_proxy="http://proxy.alcf.anl.gov:3128" fi -export HF_HOME="${HF_HOME:-/flare/ModCon/${USER}/cache/model_weights}" +export HF_HOME="${HF_HOME:-/flare//${USER}/cache/model_weights}" export HF_HUB_OFFLINE=1 export TRANSFORMERS_OFFLINE=1 export HF_DATASETS_OFFLINE=1 @@ -111,7 +114,7 @@ echo " BIOREASON GRPO (KEGG only, OLMo-1B + LoRA r16, Aurora, 1 tile)" echo " Node: $(hostname)" echo " SFT ckpt: ${STAGE1_CKPT}" echo " Run dir: ${HYDRA_RUN_DIR}" -echo " Max steps: ${MAX_STEPS} (G=${NUM_GENERATIONS}, max completion ${MAX_COMPLETION})" +echo " Overrides: ${TRAIN_OVERRIDES:- (none; using bioreason_grpo.yaml)}" echo "================================================================" mpiexec -n 1 -ppn 1 --cpu-bind depth --depth 8 bash -lc ' @@ -129,10 +132,7 @@ mpiexec -n 1 -ppn 1 --cpu-bind depth --depth 8 bash -lc ' exp.variant='"${RUN_VARIANT}"' \ training.resume_weights_only='"${STAGE1_CKPT}"' \ training.bioreason_dataset=wanglab/kegg \ - training.max_steps='"${MAX_STEPS}"' \ - training.grpo_num_generations='"${NUM_GENERATIONS}"' \ - training.grpo_max_completion_length='"${MAX_COMPLETION}"' \ - training.save_every_n_steps='"${SAVE_EVERY}"' \ + '"${TRAIN_OVERRIDES}"' \ wandb.mode='"${WANDB_MODE}"' \ hydra.run.dir='"${HYDRA_RUN_DIR}"' '"${EXTRA_HYDRA}"' ' diff --git a/applications/bioreason/scripts/training/submit_bioreason_projector.sh b/applications/bioreason/scripts/training/submit_bioreason_projector.sh index af13779..2aa4b54 100755 --- a/applications/bioreason/scripts/training/submit_bioreason_projector.sh +++ b/applications/bioreason/scripts/training/submit_bioreason_projector.sh @@ -4,7 +4,7 @@ #PBS -l walltime=01:00:00 #PBS -l filesystems=home:flare #PBS -q debug -#PBS -A ModCon +# Allocation is not hardcoded: submit with `qsub -A ...`. #PBS -k doe #PBS -j oe # @@ -15,37 +15,43 @@ # Its checkpoints/step_final is the STAGE1_CKPT for scripts/training/submit_bioreason_sft.sh. # Outputs land in ${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG} (override with HYDRA_RUN_DIR). # -# Defaults: 100 optimizer steps, grad-accum 1, warmup 10. On 12 tiles x batch 8 -# that is 96 samples/step -> 9,600 samples (~8 epochs of KEGG's 1,159 train rows). -# bioreason_projector.yaml's own grad-accum of 8 would make 100 steps ~66 epochs. +# Hyperparameters (steps, warmup, batch, grad-accum, save cadence) come from +# src/conf/training/bioreason_projector.yaml; the env vars below override them per run. # # Batch job: # qsub applications/bioreason/scripts/training/submit_bioreason_projector.sh # qsub -v MAX_STEPS=200,GRAD_ACCUM=2 applications/bioreason/scripts/training/submit_bioreason_projector.sh # -# Interactive node (after `qsub -I -l select=1,walltime=01:00:00,filesystems=home:flare -q debug -A ModCon`): +# Interactive node (after `qsub -I -l select=1,walltime=01:00:00,filesystems=home:flare -q debug -A `): # bash applications/bioreason/scripts/training/submit_bioreason_projector.sh set -eo pipefail -PROJ_DIR="/home/abalaji/projects/modcon/release/BaseMM_PRISM" +PROJ_DIR="${PROJ_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." 2>/dev/null && pwd)}" +# Under qsub the script runs from a spool copy, so BASH_SOURCE points there. +if [ -n "${PBS_O_WORKDIR:-}" ] && [ ! -f "${PROJ_DIR}/src/train.py" ]; then + PROJ_DIR="${PBS_O_WORKDIR}" +fi RUN_ID="${RUN_ID:-BIOREASON-PROJECTOR-KEGG}" RUN_VARIANT="1B" RUN_TAG="${RUN_TAG:-$(date +%Y%m%d-%H%M%S)}" -MAX_STEPS="${MAX_STEPS:-100}" -WARMUP_STEPS="${WARMUP_STEPS:-10}" -GRAD_ACCUM="${GRAD_ACCUM:-1}" -BATCH_SIZE="${BATCH_SIZE:-1}" # batch 8 at ~3k merged tokens OOMs a tile (see SFT launcher) -SAVE_EVERY="${SAVE_EVERY:-50}" +# Training hyperparameters come from src/conf/training/bioreason_projector.yaml. Set any of +# MAX_STEPS / WARMUP_STEPS / GRAD_ACCUM / BATCH_SIZE / SAVE_EVERY to override that value for one run. +TRAIN_OVERRIDES="" +[ -n "${MAX_STEPS:-}" ] && TRAIN_OVERRIDES+=" training.max_steps=${MAX_STEPS}" +[ -n "${WARMUP_STEPS:-}" ] && TRAIN_OVERRIDES+=" training.warmup_steps=${WARMUP_STEPS}" +[ -n "${GRAD_ACCUM:-}" ] && TRAIN_OVERRIDES+=" training.gradient_accumulation_steps=${GRAD_ACCUM}" +[ -n "${BATCH_SIZE:-}" ] && TRAIN_OVERRIDES+=" training.batch_size=${BATCH_SIZE}" +[ -n "${SAVE_EVERY:-}" ] && TRAIN_OVERRIDES+=" training.save_every_n_steps=${SAVE_EVERY}" NPERNODE="${NPERNODE:-12}" # 6 PVC GPUs x 2 tiles per Aurora node (FLAT mode) # Site roots: datasets_config.json resolves dna_bioreason's local_path as # ${PRISM_DATA_ROOT}/dna_local_shards/kegg. -export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects/ModCon/abalaji/cache}" -export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/abalaji/output}" +export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects//${USER}/cache}" +export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects//${USER}/output}" # Checkpoints (~13 GB each: frozen backbone + encoder are saved too) go to flare, # not Hydra's default outputs/ under the repo in /home, which overflows its quota. -export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects/ModCon/abalaji/modcon/checkpoints}" +export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects//${USER}/modcon/checkpoints}" HYDRA_RUN_DIR="${HYDRA_RUN_DIR:-${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG}}" cd "${PROJ_DIR}" @@ -56,7 +62,7 @@ cd "${PROJ_DIR}" # module load frameworks/2026.1.0 # uv venv --system-site-packages -p "$(which python)" $PRISM_VENV && source $PRISM_VENV/bin/activate # uv pip install wandb braceexpand==0.1.7 && uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 -export PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +export PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" module use /soft/modulefiles module load frameworks/2026.1.0 module load hdf5 @@ -76,7 +82,7 @@ fi # OLMo-1B-0724-hf, nucleotide-transformer-2.5b-multi-species and # datasets--wanglab--kegg are all pre-cached here; run fully offline. -export HF_HOME="/flare/ModCon/${USER}/cache/model_weights" +export HF_HOME="/flare//${USER}/cache/model_weights" export HF_HUB_OFFLINE=1 export TRANSFORMERS_OFFLINE=1 export HF_DATASETS_OFFLINE=1 @@ -122,7 +128,7 @@ echo " Node: $(hostname)" echo " Master: ${MASTER_ADDR}:${MASTER_PORT}" echo " Ranks: ${NPERNODE}" echo " KEGG shard: ${PRISM_DATA_ROOT}/dna_local_shards/kegg" -echo " Steps: ${MAX_STEPS} (batch ${BATCH_SIZE}, warmup ${WARMUP_STEPS}, grad-accum ${GRAD_ACCUM}, save every ${SAVE_EVERY})" +echo " Overrides: ${TRAIN_OVERRIDES:- (none; using bioreason_projector.yaml)}" echo " Run dir: ${HYDRA_RUN_DIR}" echo "================================================================" @@ -159,11 +165,7 @@ mpiexec -n "${NPERNODE}" -ppn "${NPERNODE}" --cpu-bind depth --depth 8 bash -lc exp.id='"${RUN_ID}"' \ exp.variant='"${RUN_VARIANT}"' \ training.bioreason_dataset=wanglab/kegg \ - training.batch_size='"${BATCH_SIZE}"' \ - training.max_steps='"${MAX_STEPS}"' \ - training.warmup_steps='"${WARMUP_STEPS}"' \ - training.gradient_accumulation_steps='"${GRAD_ACCUM}"' \ - training.save_every_n_steps='"${SAVE_EVERY}"' \ + '"${TRAIN_OVERRIDES}"' \ wandb.mode='"${WANDB_MODE}"' \ hydra.run.dir='"${HYDRA_RUN_DIR}"' ' diff --git a/applications/bioreason/scripts/training/submit_bioreason_sft.sh b/applications/bioreason/scripts/training/submit_bioreason_sft.sh index c570fbf..123fd52 100644 --- a/applications/bioreason/scripts/training/submit_bioreason_sft.sh +++ b/applications/bioreason/scripts/training/submit_bioreason_sft.sh @@ -4,7 +4,7 @@ #PBS -l walltime=06:00:00 #PBS -l filesystems=home:flare #PBS -q capacity -#PBS -A ModCon +# Allocation is not hardcoded: submit with `qsub -A ...`. #PBS -k doe #PBS -j oe # @@ -21,27 +21,33 @@ # # optional: start from a pretrained projector instead of random init # qsub -v STAGE1_CKPT=/path/to/checkpoints/step_N applications/bioreason/scripts/training/submit_bioreason_sft.sh # -# Interactive node (after `qsub -I -l select=1,walltime=01:00:00,filesystems=home:flare -q debug -A ModCon`): +# Interactive node (after `qsub -I -l select=1,walltime=01:00:00,filesystems=home:flare -q debug -A `): # bash applications/bioreason/scripts/training/submit_bioreason_sft.sh # Single-tile debug run: # NPERNODE=1 MAX_STEPS=20 bash applications/bioreason/scripts/training/submit_bioreason_sft.sh set -eo pipefail -PROJ_DIR="/home/abalaji/projects/modcon/release/BaseMM_PRISM" +PROJ_DIR="${PROJ_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." 2>/dev/null && pwd)}" +# Under qsub the script runs from a spool copy, so BASH_SOURCE points there. +if [ -n "${PBS_O_WORKDIR:-}" ] && [ ! -f "${PROJ_DIR}/src/train.py" ]; then + PROJ_DIR="${PBS_O_WORKDIR}" +fi RUN_ID="${RUN_ID:-BIOREASON-SFT-KEGG}" RUN_VARIANT="1B" RUN_TAG="${RUN_TAG:-$(date +%Y%m%d-%H%M%S)}" -MAX_STEPS="${MAX_STEPS:-500}" +# Hyperparameters come from src/conf/training/bioreason_sft.yaml; set MAX_STEPS to override max_steps. +TRAIN_OVERRIDES="" +[ -n "${MAX_STEPS:-}" ] && TRAIN_OVERRIDES+=" training.max_steps=${MAX_STEPS}" NPERNODE="${NPERNODE:-12}" # 6 PVC GPUs x 2 tiles per Aurora node (FLAT mode) # Site roots: datasets_config.json resolves dna_bioreason's local_path as # ${PRISM_DATA_ROOT}/dna_local_shards/kegg. -export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects/ModCon/abalaji/cache}" -export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects/ModCon/abalaji/output}" +export PRISM_DATA_ROOT="${PRISM_DATA_ROOT:-/lus/flare/projects//${USER}/cache}" +export PRISM_OUTPUT_ROOT="${PRISM_OUTPUT_ROOT:-/lus/flare/projects//${USER}/output}" # Checkpoints (~13 GB each) go to flare, not Hydra's default outputs/ under the # repo in /home, which overflows its quota. -export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects/ModCon/abalaji/modcon/checkpoints}" +export PRISM_CKPT_ROOT="${PRISM_CKPT_ROOT:-/lus/flare/projects//${USER}/modcon/checkpoints}" HYDRA_RUN_DIR="${HYDRA_RUN_DIR:-${PRISM_CKPT_ROOT}/${RUN_ID}/${RUN_TAG}}" # Optional: Stage 1 (bioreason_projector, model=prism_olmo_1b) checkpoint. Unset @@ -61,7 +67,7 @@ cd "${PROJ_DIR}" # module load frameworks/2026.1.0 # uv venv --system-site-packages -p "$(which python)" $PRISM_VENV && source $PRISM_VENV/bin/activate # uv pip install wandb braceexpand==0.1.7 && uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 -export PRISM_VENV="${PRISM_VENV:-/flare/ModCon/${USER}/prism-envs/bioreason-fw2026.1}" +export PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" module use /soft/modulefiles module load frameworks/2026.1.0 module load hdf5 @@ -80,7 +86,7 @@ fi # OLMo-1B-0724-hf, nucleotide-transformer-2.5b-multi-species and # datasets--wanglab--kegg are all pre-cached here; run fully offline. -export HF_HOME="/flare/ModCon/${USER}/cache/model_weights" +export HF_HOME="/flare//${USER}/cache/model_weights" export HF_HUB_OFFLINE=1 export TRANSFORMERS_OFFLINE=1 export HF_DATASETS_OFFLINE=1 @@ -130,7 +136,7 @@ echo " Master: ${MASTER_ADDR}:${MASTER_PORT}" echo " Ranks: ${NPERNODE}" echo " Projector init: ${STAGE1_CKPT:-random (no Stage 1)}" echo " KEGG shard: ${PRISM_DATA_ROOT}/dna_local_shards/kegg" -echo " Max steps: ${MAX_STEPS}" +echo " Overrides: ${TRAIN_OVERRIDES:- (none; using bioreason_sft.yaml)}" echo " Run dir: ${HYDRA_RUN_DIR}" echo "================================================================" @@ -168,7 +174,7 @@ mpiexec -n "${NPERNODE}" -ppn "${NPERNODE}" --cpu-bind depth --depth 8 bash -lc exp.variant='"${RUN_VARIANT}"' \ '"${RESUME_ARG}"' \ training.bioreason_dataset=wanglab/kegg \ - training.max_steps='"${MAX_STEPS}"' \ + '"${TRAIN_OVERRIDES}"' \ wandb.mode='"${WANDB_MODE}"' \ hydra.run.dir='"${HYDRA_RUN_DIR}"' ' From 224f9be3352b4dbf72da224f3e377717d28d66c2 Mon Sep 17 00:00:00 2001 From: Adarsha Balaji Date: Tue, 13 Oct 2026 21:46:55 +0000 Subject: [PATCH 6/8] bioreason-demo: use torchtune-aurora fork (torchtune.dev GRPO loss) and single data worker for GRPO Co-Authored-By: Claude Sonnet 5.5 --- applications/bioreason/bioreason_demo.ipynb | 10 +++++++--- applications/bioreason/helpers/setup_prism_env.sh | 11 +++++++++-- .../scripts/training/submit_bioreason_projector.sh | 5 ++++- .../scripts/training/submit_bioreason_sft.sh | 5 ++++- src/conf/training/bioreason_grpo.yaml | 2 +- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/applications/bioreason/bioreason_demo.ipynb b/applications/bioreason/bioreason_demo.ipynb index 1c297f4..e319e4b 100644 --- a/applications/bioreason/bioreason_demo.ipynb +++ b/applications/bioreason/bioreason_demo.ipynb @@ -74,7 +74,7 @@ "cd PRISM && git checkout prism-bioreason-demo\n", "\n", "# Environment: loads frameworks/2026.1.0, creates a --system-site-packages venv at $PRISM_VENV,\n", - "# installs the extras the module lacks (torchtune, webdataset, wandb) and PRISM itself (--no-deps).\n", + "# installs the extras the module lacks (torchtune from the torchtune-aurora fork, webdataset, wandb) and PRISM itself (--no-deps).\n", "export PRISM_VENV=/flare/$PROJECT/$USER/prism-envs/bioreason-fw2026.1\n", "bash applications/bioreason/helpers/setup_prism_env.sh\n", "```\n", @@ -244,7 +244,7 @@ "source": [ "## 2. Prerequisites\n", "\n", - "PRISM on Aurora uses the **`frameworks/2026.1.0`** module (XPU PyTorch, oneCCL) plus a thin `--system-site-packages` venv holding only what the module lacks: `torchtune` for LoRA, `webdataset`, `wandb`, and `ipywidgets` for this notebook. Extras are installed with `--no-deps` so pip never swaps the module's XPU torch for a CUDA wheel.\n", + "PRISM on Aurora uses the **`frameworks/2026.1.0`** module (XPU PyTorch, oneCCL) plus a thin `--system-site-packages` venv holding only what the module lacks: `torchtune` (the [n-getty/torchtune-aurora](https://github.com/n-getty/torchtune-aurora) fork, for LoRA and the GRPO loss), `webdataset`, `wandb`, and `ipywidgets` for this notebook. Extras are installed with `--no-deps` so pip never swaps the module's XPU torch for a CUDA wheel.\n", "\n", "> Do **not** activate the repo's `.venv`. It carries a CUDA torch build, and its transformers 5.x can't run the Nucleotide Transformer **v2** remote code (see section 7)." ] @@ -289,7 +289,11 @@ "# Only needed if something is listed as missing above (same recipe as the submit scripts' header).\n", "if missing:\n", " !uv pip install wandb braceexpand==0.1.7 ipywidgets\n", - " !uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1\n", + " !uv pip install --no-deps webdataset==1.0.2 torchao==0.5.0\n", + " # torchtune: the n-getty fork (PyPI's has no torchtune.dev.rl, needed by GRPO)\n", + " TT = Path(PRISM_VENV).parent / \"torchtune-aurora\"\n", + " if not TT.exists(): !git clone https://github.com/n-getty/torchtune-aurora.git {TT}\n", + " !uv pip install --no-deps -e {TT}\n", " !uv pip install --no-deps -e {REPO}\n", " print(\"Installed - restart the kernel and re-run section 1.\")" ] diff --git a/applications/bioreason/helpers/setup_prism_env.sh b/applications/bioreason/helpers/setup_prism_env.sh index d8e101f..312f564 100755 --- a/applications/bioreason/helpers/setup_prism_env.sh +++ b/applications/bioreason/helpers/setup_prism_env.sh @@ -3,7 +3,7 @@ # # Builds (or reuses) the BioReason venv on top of frameworks/2026.1.0 -- the same venv the # scripts/training/submit_bioreason_*.sh scripts activate -- and installs the training extras -# the frameworks module lacks plus PRISM itself (--no-deps, so the module's XPU torch stays). +# the frameworks module lacks (torchtune from the torchtune-aurora fork) plus PRISM itself (--no-deps, so the module's XPU torch stays). # # Run once from a login node (UAN; it has direct internet access): # export PRISM_VENV=/flare//$USER/prism-envs/bioreason-fw2026.1 @@ -35,7 +35,14 @@ source "${PRISM_VENV}/bin/activate" # Same extras as the recipe in submit_bioreason_sft.sh; --no-deps keeps the # module's XPU torch from being replaced by a PyPI/CUDA wheel. uv pip install wandb braceexpand==0.1.7 -uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 +uv pip install --no-deps webdataset==1.0.2 torchao==0.5.0 # torchao pin: the fork's torchtune imports torchao.dtypes.nf4tensor + +# torchtune: use the n-getty/torchtune-aurora fork, NOT the PyPI wheel. The PyPI torchtune has no +# torchtune.dev.rl, which src/training/trainer_grpo.py needs (GRPOSimpleLoss); the fork also provides +# the LoRA modules SFT uses. Cloned next to the venv; override the location with TORCHTUNE_DIR. +TORCHTUNE_DIR="${TORCHTUNE_DIR:-$(dirname "${PRISM_VENV}")/torchtune-aurora}" +[ -d "${TORCHTUNE_DIR}/.git" ] || git clone https://github.com/n-getty/torchtune-aurora.git "${TORCHTUNE_DIR}" +uv pip install --no-deps -e "${TORCHTUNE_DIR}" uv pip install --no-deps -e "${PRISM_DIR}" echo "PRISM environment ready:" diff --git a/applications/bioreason/scripts/training/submit_bioreason_projector.sh b/applications/bioreason/scripts/training/submit_bioreason_projector.sh index 2aa4b54..3622435 100755 --- a/applications/bioreason/scripts/training/submit_bioreason_projector.sh +++ b/applications/bioreason/scripts/training/submit_bioreason_projector.sh @@ -61,7 +61,10 @@ cd "${PROJ_DIR}" # which carries a CUDA torch build. Build it once (from a UAN) with: # module load frameworks/2026.1.0 # uv venv --system-site-packages -p "$(which python)" $PRISM_VENV && source $PRISM_VENV/bin/activate -# uv pip install wandb braceexpand==0.1.7 && uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 +# uv pip install wandb braceexpand==0.1.7 && uv pip install --no-deps webdataset==1.0.2 torchao==0.5.0 +# # torchtune comes from the n-getty fork (the PyPI wheel lacks torchtune.dev.rl, which GRPO needs): +# git clone https://github.com/n-getty/torchtune-aurora.git $(dirname $PRISM_VENV)/torchtune-aurora +# uv pip install --no-deps -e $(dirname $PRISM_VENV)/torchtune-aurora export PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" module use /soft/modulefiles module load frameworks/2026.1.0 diff --git a/applications/bioreason/scripts/training/submit_bioreason_sft.sh b/applications/bioreason/scripts/training/submit_bioreason_sft.sh index 123fd52..f203635 100644 --- a/applications/bioreason/scripts/training/submit_bioreason_sft.sh +++ b/applications/bioreason/scripts/training/submit_bioreason_sft.sh @@ -66,7 +66,10 @@ cd "${PROJ_DIR}" # which carries a CUDA torch build. Build it once (from a UAN) with: # module load frameworks/2026.1.0 # uv venv --system-site-packages -p "$(which python)" $PRISM_VENV && source $PRISM_VENV/bin/activate -# uv pip install wandb braceexpand==0.1.7 && uv pip install --no-deps webdataset==1.0.2 torchtune==0.6.1 +# uv pip install wandb braceexpand==0.1.7 && uv pip install --no-deps webdataset==1.0.2 torchao==0.5.0 +# # torchtune comes from the n-getty fork (the PyPI wheel lacks torchtune.dev.rl, which GRPO needs): +# git clone https://github.com/n-getty/torchtune-aurora.git $(dirname $PRISM_VENV)/torchtune-aurora +# uv pip install --no-deps -e $(dirname $PRISM_VENV)/torchtune-aurora export PRISM_VENV="${PRISM_VENV:-/flare//${USER}/prism-envs/bioreason-fw2026.1}" module use /soft/modulefiles module load frameworks/2026.1.0 diff --git a/src/conf/training/bioreason_grpo.yaml b/src/conf/training/bioreason_grpo.yaml index 35e4178..d4306ff 100644 --- a/src/conf/training/bioreason_grpo.yaml +++ b/src/conf/training/bioreason_grpo.yaml @@ -56,7 +56,7 @@ grpo_reward_functions: - correctness # Data -data_num_workers: 4 +data_num_workers: 1 # KEGG local shard is a single Arrow file; extra workers get no shard and the stream looks empty bioreason_dataset: "wanglab/kegg" max_dna_length: 1024 max_text_length: 1024 From 8d47dd7588eccd5fd0e895d94e33996ece647bdd Mon Sep 17 00:00:00 2001 From: Adarsha Balaji Date: Tue, 13 Oct 2026 22:04:59 +0000 Subject: [PATCH 7/8] CI: fix lint, mypy, site-path allowlist and tests for the bioreason-demo changes - ruff: notebook one-liners/magics, lambda, zip strict, ambiguous name - mypy: annotate build_collator's collator - regenerate tools/ci/site_path_allowlist.json - multimodal: tolerate tokenizers without eos_token (fallback <|endoftext|>) - tests: KEGG-only default, OLMo Tulu turn markers Co-Authored-By: Claude Sonnet 5.5 --- applications/bioreason/bioreason_demo.ipynb | 64 ++++++++++++++----- .../bioreason/helpers/demo_helpers.py | 6 +- src/data/multimodal.py | 2 +- src/train.py | 1 + .../test_dna_bioreason_datasets_config.py | 16 +++-- .../test_variant_effect_coding_handler.py | 8 +-- .../test_variant_effect_non_snv_handler.py | 4 +- tools/ci/site_path_allowlist.json | 8 +-- 8 files changed, 70 insertions(+), 39 deletions(-) diff --git a/applications/bioreason/bioreason_demo.ipynb b/applications/bioreason/bioreason_demo.ipynb index e319e4b..62b56ca 100644 --- a/applications/bioreason/bioreason_demo.ipynb +++ b/applications/bioreason/bioreason_demo.ipynb @@ -187,7 +187,10 @@ } ], "source": [ - "import os, sys, getpass, datetime\n", + "import datetime\n", + "import getpass\n", + "import os\n", + "import sys\n", "from pathlib import Path\n", "\n", "USER = getpass.getuser()\n", @@ -229,7 +232,7 @@ "sys.path.insert(0, str(REPO))\n", "sys.path.insert(0, str(REPO / \"applications\" / \"bioreason\"))\n", "sys.path.insert(0, str(REPO / \"applications\" / \"bioreason\" / \"helpers\"))\n", - "import demo_helpers as H # applications/bioreason/helpers/demo_helpers.py\n", + "import demo_helpers as H # applications/bioreason/helpers/demo_helpers.py\n", "\n", "print(\"repo \", REPO)\n", "print(\"run tag\", RUN_TAG)\n", @@ -268,7 +271,10 @@ } ], "source": [ - "import importlib, torch, transformers\n", + "import importlib\n", + "\n", + "import torch\n", + "import transformers\n", "\n", "print(\"python \", sys.executable)\n", "print(\"in venv \", Path(sys.prefix).resolve() == Path(PRISM_VENV).resolve(), f\"(sys.prefix={sys.prefix})\")\n", @@ -292,7 +298,8 @@ " !uv pip install --no-deps webdataset==1.0.2 torchao==0.5.0\n", " # torchtune: the n-getty fork (PyPI's has no torchtune.dev.rl, needed by GRPO)\n", " TT = Path(PRISM_VENV).parent / \"torchtune-aurora\"\n", - " if not TT.exists(): !git clone https://github.com/n-getty/torchtune-aurora.git {TT}\n", + " if not TT.exists():\n", + " !git clone https://github.com/n-getty/torchtune-aurora.git {TT}\n", " !uv pip install --no-deps -e {TT}\n", " !uv pip install --no-deps -e {REPO}\n", " print(\"Installed - restart the kernel and re-run section 1.\")" @@ -442,8 +449,8 @@ } ], "source": [ - "from datasets import load_from_disk\n", "import matplotlib.pyplot as plt\n", + "from datasets import load_from_disk\n", "from IPython.display import HTML, display\n", "\n", "DS = {k: load_from_disk(str(SHARDS / k)) for k in (\"kegg\", \"variant_effect_coding\", \"variant_effect_non_snv\")}\n", @@ -695,12 +702,14 @@ ], "source": [ "from transformers import AutoTokenizer\n", + "\n", "llm_tok = AutoTokenizer.from_pretrained(\"tokenizers/prism-olmo-1b-interleaved\")\n", "dna_tok = AutoTokenizer.from_pretrained(SNAPSHOTS[\"dna\"], trust_remote_code=True)\n", "\n", "LEN = H.length_frame({k: DS[k] for k in (\"kegg\", \"variant_effect_coding\", \"variant_effect_non_snv\")},\n", " llm_tok, dna_tok, max_rows=1500)\n", - "H.plot_lengths(LEN, max_dna_length=1024); plt.show()\n", + "H.plot_lengths(LEN, max_dna_length=1024)\n", + "plt.show()\n", "display(LEN.groupby(\"dataset\").describe().T.loc[(slice(None), [\"mean\", \"50%\", \"max\"]), :].round(0))\n", "print(\"share of reference spans truncated at 1024 tokens:\",\n", " LEN.assign(t=LEN.ref_dna_tokens > 1024).groupby(\"dataset\").t.mean().round(3).to_dict())" @@ -732,7 +741,9 @@ ], "source": [ "from collections import Counter\n", + "\n", "from src.data.multimodal import clean_kegg_answer\n", + "\n", "answers = Counter(clean_kegg_answer(a) for a in DS[\"kegg\"][\"answer\"])\n", "print(f\"{len(answers)} distinct KEGG answers; top 15 cover {sum(c for _, c in answers.most_common(15)) / len(DS['kegg']):.0%}\")\n", "H.plot_counts(dict(answers.most_common(15)), \"KEGG train: most frequent answers (after clean_kegg_answer)\");" @@ -820,6 +831,7 @@ "outputs": [], "source": [ "import json\n", + "\n", "zone_a = json.load(open(REPO / \"src/data/datasets_config.json\"))[\"zone_a\"][\"datasets\"]\n", "enabled = {k: v[\"hf_id\"] for k, v in zone_a.items() if not v.get(\"skip\")}\n", "print(\"enabled zone-A datasets:\", enabled)\n" @@ -1002,12 +1014,14 @@ ], "source": [ "import glob\n", - "from hydra import initialize_config_dir, compose\n", - "from hydra.core.global_hydra import GlobalHydra\n", + "\n", "import pandas as pd\n", "import src.train as T\n", + "from hydra import compose, initialize_config_dir\n", + "from hydra.core.global_hydra import GlobalHydra\n", "from src.data.dataset_manager import DatasetManager\n", "\n", + "\n", "def compose_cfg(*overrides):\n", " # Same composition as `python src/train.py `.\n", " GlobalHydra.instance().clear()\n", @@ -1460,9 +1474,10 @@ } ], "source": [ - "from transformers import AutoTokenizer\n", "from datasets import load_from_disk\n", "from src.encoders.dna import DNAEncoder\n", + "from transformers import AutoTokenizer\n", + "\n", "dev = torch.device(\"xpu:0\")\n", "\n", "# The DNA tokenizer comes from the SAME checkpoint as the encoder (what the data pipeline does).\n", @@ -1478,7 +1493,8 @@ "print(\"added projection \", getattr(enc, \"proj\", None))\n", "print(\"tokens_per_instance \", enc.tokens_per_instance())\n", "print(f\"{len(seq):,} bp -> input_ids {tuple(ids['input_ids'].shape)} -> embeddings {tuple(out.shape)}\")\n", - "del enc, out; torch.xpu.empty_cache()" + "del enc, out\n", + "torch.xpu.empty_cache()" ] }, { @@ -1570,6 +1586,7 @@ "import pandas as pd\n", "from IPython.display import display\n", "\n", + "\n", "def nt_vocab(snapshot):\n", " # Token order of an NT checkpoint, read straight from its vocab.txt (line i = id i).\n", " return {tok: i for i, tok in enumerate(Path(snapshot, \"vocab.txt\").read_text().split())}\n", @@ -2172,6 +2189,7 @@ ], "source": [ "from src.utils.lora_utils import apply_lora_torchtune\n", + "\n", "MOD = [\"model.projector_hidden_mult=2\", \"model.projector_num_layers=3\"]\n", "LORA = dict(rank=16, alpha=32.0)\n", "\n", @@ -2257,6 +2275,7 @@ "source": [ "# Config diff (what you would put on the train.py command line)\n", "from omegaconf import OmegaConf\n", + "\n", "a, b = OmegaConf.to_container(cfg.model), OmegaConf.to_container(cfg_mod.model)\n", "display(pd.DataFrame([(k, a.get(k), b.get(k)) for k in sorted(set(a) | set(b)) if a.get(k) != b.get(k)]\n", " + [(\"training.lora_enabled / r / alpha\", \"false\", f\"true / {LORA['rank']} / {LORA['alpha']:g}\")],\n", @@ -2530,8 +2549,10 @@ ], "source": [ "def to_dev(x):\n", - " if torch.is_tensor(x): return x.to(dev)\n", - " if isinstance(x, dict): return {k: to_dev(v) for k, v in x.items()}\n", + " if torch.is_tensor(x):\n", + " return x.to(dev)\n", + " if isinstance(x, dict):\n", + " return {k: to_dev(v) for k, v in x.items()}\n", " return x\n", "\n", "model.to(dev).eval()\n", @@ -2571,7 +2592,10 @@ ], "source": [ "del model, logits, loss, batch\n", - "import gc; gc.collect(); torch.xpu.empty_cache()\n", + "import gc\n", + "\n", + "gc.collect()\n", + "torch.xpu.empty_cache()\n", "\n", "SCRIPTS = Path(PRISM_OUTPUT_ROOT, \"demo_scripts\", RUN_TAG)\n", "PROJ_SH = H.patch_stage_script(REPO / \"applications/bioreason/scripts/training/submit_bioreason_projector.sh\", SCRIPTS / \"projector.sh\")\n", @@ -2649,7 +2673,8 @@ } ], "source": [ - "H.plot_logs({\"smoke projector\": SMOKE_LOG}); plt.show()\n", + "H.plot_logs({\"smoke projector\": SMOKE_LOG})\n", + "plt.show()\n", "!ls {SMOKE_DIR}/checkpoints/step_final" ] }, @@ -2717,7 +2742,9 @@ "common = {k: ENV[k] for k in (\"PRISM_DATA_ROOT\", \"PRISM_OUTPUT_ROOT\", \"PRISM_CKPT_ROOT\", \"PRISM_VENV\", \"WANDB_MODE\", \"MAX_SEQ_LENGTH\")} | {\"RUN_TAG\": RUN_TAG}\n", "\n", "JOBS = {}\n", - "q = lambda *a, **k: H.qsub(*a, cwd=str(REPO), dry_run=not SUBMIT, account=PROJECT, **k)\n", + "def q(*a, **k):\n", + " return H.qsub(*a, cwd=str(REPO), dry_run=not SUBMIT, account=PROJECT, **k)\n", + "\n", "JOBS[\"projector\"] = q(PROJ_SH, {**common, \"HYDRA_RUN_DIR\": RUN[\"projector\"], \"MAX_STEPS\": 100, \"SAVE_EVERY\": 50},\n", " log=LOGS[\"projector\"])\n", "JOBS[\"sft\"] = q(SFT_SH, {**common, \"HYDRA_RUN_DIR\": RUN[\"sft\"], \"STAGE1_CKPT\": CKPT_PROJ, \"MAX_STEPS\": 500},\n", @@ -2793,6 +2820,7 @@ ], "source": [ "import json\n", + "\n", "res = {k: json.load(open(p)) for k, p in EVAL_OUT.items() if Path(p).exists()}\n", "if not res:\n", " print(\"no eval results yet\")\n", @@ -3041,14 +3069,16 @@ } ], "source": [ - "from eval_kegg_aurora import load_eval_model, evaluate\n", + "from eval_kegg_aurora import evaluate, load_eval_model\n", + "\n", "ckpt = CKPT_SFT if Path(CKPT_SFT).exists() else (f\"{SMOKE_DIR}/checkpoints/step_final\" if Path(SMOKE_DIR).exists() else None)\n", "print(\"checkpoint:\", ckpt)\n", "m, ecfg = load_eval_model(ckpt, overrides=[f\"+model.dna_model_name={DNA_ENCODER}\"], device=dev)\n", "quick = evaluate(m, ecfg, DS[\"kegg (test)\"], limit=5, max_new_tokens=128)\n", "print({k: quick[k] for k in (\"accuracy\", \"format_rate\", \"n\")})\n", "print(quick[\"records\"][0][\"completion\"][:500])\n", - "del m; torch.xpu.empty_cache()" + "del m\n", + "torch.xpu.empty_cache()" ] }, { diff --git a/applications/bioreason/helpers/demo_helpers.py b/applications/bioreason/helpers/demo_helpers.py index 3ce7776..543310f 100644 --- a/applications/bioreason/helpers/demo_helpers.py +++ b/applications/bioreason/helpers/demo_helpers.py @@ -63,7 +63,7 @@ def show_source(path: str | Path, start: int | None = None, end: int | None = No lines = p.read_text().splitlines() if pattern is not None: rx = re.compile(pattern) - hit = next((i for i, l in enumerate(lines) if rx.search(l)), None) + hit = next((i for i, ln in enumerate(lines) if rx.search(ln)), None) if hit is None: raise ValueError(f"{pattern!r} not found in {path}") start, end = hit + 1, min(len(lines), hit + context) @@ -204,7 +204,7 @@ def plot_lengths(df, max_dna_length: int = 1024): ("reasoning_tokens", "Reasoning tokens (OLMo, KEGG only)"), ("answer_tokens", "Answer tokens (OLMo)"), ] - for ax, (col, title) in zip(axes[1:], panels): + for ax, (col, title) in zip(axes[1:], panels, strict=False): for i, name in enumerate(names): vals = df.loc[df.dataset == name, col] if vals.max() == 0: @@ -468,7 +468,7 @@ def plot_logs(logs: dict): panels = [(k, "loss", r) for k, r in runs.items()] panels += [(k, "reward", r) for k, r in runs.items() if r["reward"]] fig, axes = plt.subplots(1, len(panels), figsize=(4.2 * len(panels), 3), squeeze=False) - for ax, (name, key, r) in zip(axes[0], panels): + for ax, (name, key, r) in zip(axes[0], panels, strict=False): if r[key]: ax.plot(r["step"], r[key], color=SERIES[0] if key == "loss" else SERIES[2], linewidth=2) ax.annotate(f"{r[key][-1]:.3f}", (r["step"][-1], r[key][-1]), fontsize=9, color=INK, diff --git a/src/data/multimodal.py b/src/data/multimodal.py index 5ccc972..64edce3 100644 --- a/src/data/multimodal.py +++ b/src/data/multimodal.py @@ -2728,7 +2728,7 @@ def _tag_str(tag): _tag_str(t) for t in modality_tags.get("dna_variant", ("", "")) ) - eos = self.tokenizer.eos_token # OLMo "<|endoftext|>" + eos = getattr(self.tokenizer, "eos_token", None) or "<|endoftext|>" # OLMo EOS dna_ref_placeholder = f"Reference sequence: {dna_ref_tags[0]}{dna_ref_tags[1]}\n" dna_var_placeholder = f"Variant sequence: {dna_var_tags[0]}{dna_var_tags[1]}\n" diff --git a/src/train.py b/src/train.py index 820d8a9..451175e 100644 --- a/src/train.py +++ b/src/train.py @@ -522,6 +522,7 @@ def build_collator( ): """Pick the Bucketed/Multimodal collator for the non-VLA streaming path.""" _is_dynamic_length_ts = model_config.ts_projector in DYNAMIC_LENGTH_TS_PROJECTORS + collator: BucketedCollator | MultimodalCollator if use_bucketed_collator and not _is_dynamic_length_ts: collator = BucketedCollator( tokenizer, diff --git a/tests/data/test_dna_bioreason_datasets_config.py b/tests/data/test_dna_bioreason_datasets_config.py index 465c84a..2879748 100644 --- a/tests/data/test_dna_bioreason_datasets_config.py +++ b/tests/data/test_dna_bioreason_datasets_config.py @@ -47,9 +47,13 @@ def test_handler_field_matches_key(zone_a_datasets): assert zone_a_datasets[key]["handler"] == key -def test_none_are_skipped(zone_a_datasets): - for key in DNA_HANDLER_KEYS: - assert zone_a_datasets[key]["skip"] is False, f"{key} is marked skip=true" +def test_bioreason_demo_default_is_kegg_only(zone_a_datasets): + # The bioreason demo trains on KEGG only: dna_bioreason is enabled and the two + # variant-effect datasets are skipped by default (flip skip to false, or use + # data.dataset_overrides, to mix them back in). + assert zone_a_datasets["dna_bioreason"]["skip"] is False + for key in ("dna_bioreason_variant_effect_coding", "dna_bioreason_variant_effect_non_snv"): + assert zone_a_datasets[key]["skip"] is True, f"{key} should be skip=true in the demo default" def test_all_dna_handlers_contain_dna_substring(zone_a_datasets): @@ -119,10 +123,8 @@ def test_modality_group_normalization_matches_multimodal_py(): modality_groups[target].append((name, w)) dna_group = dict(modality_groups["dna"]) - assert set(dna_group.keys()) == set(DNA_HANDLER_KEYS.keys()) + assert set(dna_group.keys()) == {"dna_bioreason"} total = sum(dna_group.values()) normalized = {k: v / total for k, v in dna_group.items()} - assert normalized["dna_bioreason"] == pytest.approx(0.50) - assert normalized["dna_bioreason_variant_effect_coding"] == pytest.approx(0.25) - assert normalized["dna_bioreason_variant_effect_non_snv"] == pytest.approx(0.25) + assert normalized["dna_bioreason"] == pytest.approx(1.0) diff --git a/tests/data/test_variant_effect_coding_handler.py b/tests/data/test_variant_effect_coding_handler.py index 3c01340..68d2b48 100644 --- a/tests/data/test_variant_effect_coding_handler.py +++ b/tests/data/test_variant_effect_coding_handler.py @@ -147,12 +147,12 @@ def test_sft_true_appends_answer_tagged_assistant_turn_with_no_reasoning(): # never "Answer: X" (BioReason's literal format) — see handler docstring. assert "" not in caption assert "pathogenic" in caption - assert "<|im_start|>assistant" in caption + assert "<|assistant|>" in caption # The "promptlen targetlen" split is computed in __iter__ from these two # renderings, so assert the property it depends on: the prompt stops at the # assistant marker and the answer lands strictly after it. full_text, prompt_text = ds._render_dna_prompt_text(result["prompt"]) - assert prompt_text.endswith("<|im_start|>assistant\n") + assert prompt_text.endswith("<|assistant|>\n") assert len(full_text) > len(prompt_text) assert "pathogenic" not in prompt_text @@ -163,7 +163,7 @@ def test_sft_false_omits_assistant_turn_for_grpo(): caption = render(ds, result) # The generation marker is still emitted (that is what makes it a generation # prompt); what must be absent is the answer text after it. - assert "<|im_start|>assistant" in caption + assert "<|assistant|>" in caption assert "" not in caption # generation-prompt only, no answer text # The answer still surfaces for reward computation, but as a real key -- # trainer_grpo.py reads batch["answer"] rather than parsing a metadata tag. @@ -191,7 +191,7 @@ def test_none_item_returns_safe_fallback(): # fallback ref/var = ("CTGA", "CTGA") in dna-llm mode, so there is still a # real pair for __iter__ to encode. assert result["dna_sequences"] == ["CTGA", "CTGA"] - assert "<|im_start|>assistant" in caption # is_sft=True still appends fallback assistant turn + assert "<|assistant|>" in caption # is_sft=True still appends fallback assistant turn def test_none_item_with_is_sft_false_omits_assistant_turn(): diff --git a/tests/data/test_variant_effect_non_snv_handler.py b/tests/data/test_variant_effect_non_snv_handler.py index 56a4c60..63ecf83 100644 --- a/tests/data/test_variant_effect_non_snv_handler.py +++ b/tests/data/test_variant_effect_non_snv_handler.py @@ -153,7 +153,7 @@ def test_sft_false_omits_assistant_turn_for_grpo(): caption = render(ds, ds._process_dna_bioreason_variant_effect_non_snv(REAL_SAMPLE_ITEM_WITH_TERMS)) # The generation marker is still emitted (the renderer always opens the # assistant turn); what is absent is the answer the model must produce. - assert "<|im_start|>assistant" in caption + assert "<|assistant|>" in caption assert "" not in caption @@ -177,7 +177,7 @@ def test_none_item_returns_safe_fallback(): assert "No variant information available." in caption # dna-llm still needs a well-formed pair for __iter__ to tokenize. assert result["dna_sequences"] == ["CTGA", "CTGA"] - assert "<|im_start|>assistant" in caption + assert "<|assistant|>" in caption def test_none_item_with_is_sft_false_omits_assistant_turn(): diff --git a/tools/ci/site_path_allowlist.json b/tools/ci/site_path_allowlist.json index 663b241..35ee1f2 100644 --- a/tools/ci/site_path_allowlist.json +++ b/tools/ci/site_path_allowlist.json @@ -1,11 +1,9 @@ { "_comment": "Per-file counts of hardcoded site paths, as a ratchet. See tools/ci/check_site_paths.py. Counts may only decrease; regenerate with `python tools/ci/check_site_paths.py --write`.", "files": { - "applications/bioreason/scripts/training/submit_bioreason_grpo.sh": 8, - "applications/bioreason/scripts/training/submit_bioreason_projector.sh": 7, - "applications/bioreason/scripts/training/submit_bioreason_sft.sh": 10, - "applications/bioreason/scripts/eval/submit_eval_kegg.sh": 8, - "applications/bioreason/scripts/test/verify_dna_dataset_mix.sh": 7, + "applications/bioreason/scripts/eval/submit_eval_kegg.sh": 7, + "applications/bioreason/scripts/test/verify_dna_dataset_mix.sh": 6, + "applications/bioreason/scripts/training/submit_bioreason_grpo.sh": 7, "applications/material_science/stage_materials_text_model.py": 1, "applications/text/convert_arxiv_streaming.py": 6, "applications/text/convert_nemotron_to_webdataset.py": 4, From c9e4b08baaeef550cebc87f20632fae256337221 Mon Sep 17 00:00:00 2001 From: Adarsha Balaji Date: Tue, 13 Oct 2026 22:11:30 +0000 Subject: [PATCH 8/8] tests: sample 32 items in the requested-modality contract test With the KEGG-only default the dataset mix changed, so 4 draws could miss the requested modality by chance. Co-Authored-By: Claude Sonnet 5.5 --- tests/multimodal/test_modality_contracts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/multimodal/test_modality_contracts.py b/tests/multimodal/test_modality_contracts.py index 90922f3..405b2da 100644 --- a/tests/multimodal/test_modality_contracts.py +++ b/tests/multimodal/test_modality_contracts.py @@ -411,7 +411,7 @@ def decode(self, token_ids): ds = StreamingMultimodalDataset( tokenizer=_Tok(), batch_size=1, - max_steps=4, + max_steps=32, model_config=cfg, allow_dummy_data=True, # fallback so missing local data doesn't hard-fail ) @@ -426,13 +426,13 @@ def decode(self, token_ids): try: for i, sample in enumerate(ds): seen_keys.update(sample.keys()) - if i >= 3: + if i >= 31: break except Exception as exc: pytest.skip(f"StreamingMultimodalDataset iter failed (likely HF offline): {exc}") assert modality in seen_keys, ( - f"Pulled 4 samples; requested modality {modality!r} never appeared. " + f"Pulled 32 samples; requested modality {modality!r} never appeared. " f"Saw keys: {sorted(seen_keys)}" )

JKfC=#)Jg}p{l$3C_aLZdPv(h5B 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