Instructions to use ryoji-info/Gemma-4-12B-PsiLM with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use ryoji-info/Gemma-4-12B-PsiLM with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir Gemma-4-12B-PsiLM ryoji-info/Gemma-4-12B-PsiLM
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Gemma-4-12B-PsiLM: bridges, Burgers FNO, inference script, model card
Browse files- .gitattributes +1 -0
- MANIFEST.md +77 -0
- README.md +182 -0
- bridges/gemma-4-12b-4bit-mlx-1d-value-selective/bridges.safetensors +3 -0
- bridges/gemma-4-12b-4bit-mlx-1d-value-selective/config.json +47 -0
- physics/fno_burgers_singlemode.safetensors +3 -0
- psilm-banner.png +3 -0
- psilm_infer.py +364 -0
- requirements.txt +18 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
psilm-banner.png filter=lfs diff=lfs merge=lfs -text
|
MANIFEST.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MANIFEST — Hugging Face repo `ryoji-info/Gemma-4-12B-PsiLM`
|
| 2 |
+
|
| 3 |
+
Everything in this directory (`release/gemma-4-12b-psilm/`) is uploaded as-is to the
|
| 4 |
+
model repo; nothing outside it is needed. The backbone (`mlx-community/gemma-4-12B-it-4bit`)
|
| 5 |
+
is **not** uploaded — the script downloads it at first run.
|
| 6 |
+
|
| 7 |
+
## Files to upload (and where each one came from)
|
| 8 |
+
|
| 9 |
+
| path in the HF repo | source in the GitHub checkout | size | sha256 |
|
| 10 |
+
|---|---|---:|---|
|
| 11 |
+
| `README.md` | written for this release (the HF model card, YAML front matter) | 15 KB | — |
|
| 12 |
+
| `psilm_infer.py` | written for this release | 18 KB | — |
|
| 13 |
+
| `requirements.txt` | written for this release | 1 KB | — |
|
| 14 |
+
| `MANIFEST.md` | this file (harmless to upload; drop it if you prefer) | — | — |
|
| 15 |
+
| `psilm-banner.png` | `assets/psilm-banner.png` (1600 px wide) | 1.35 MB | `4cd65f32fe4aab5b49b66681a841b0128628e8d94c75494ae84f941b49c48089` |
|
| 16 |
+
| `bridges/gemma-4-12b-4bit-mlx-1d-value-selective/bridges.safetensors` | `results/hf_export/bridges/gemma-4-12b-4bit-mlx-1d-value-selective/bridges.safetensors` (= `results/stage2_gemma12b/bridges.npz`, step 7000, learned-pointer tensors dropped) | 102,068,660 B | `f6ef8946c41b3cfa17df7c22bcab2c5cffbd856c2dd608970a364cc14d5e8f7d` |
|
| 17 |
+
| `bridges/gemma-4-12b-4bit-mlx-1d-value-selective/config.json` | `results/hf_export/bridges/gemma-4-12b-4bit-mlx-1d-value-selective/config.json` | 1,287 B | `8a5add382beb65a0214f5e8b0b640e5d5df6f19e9a6caf1945400d2fe1e6bba5` |
|
| 18 |
+
| `physics/fno_burgers_singlemode.safetensors` | `results/hf_export/physics/fno_burgers_singlemode.safetensors` (= `results/stage2/fno.pt`; loaders verified identical, max weight and field difference 0.0) | 552,076 B | `7bb0076c85cdcf2505a9079c05964e3eb77ac4a776eccf34216953f3c37bfcdd` |
|
| 19 |
+
|
| 20 |
+
The copies were made with `cp` on 2026-09-06 and the hashes match the sources
|
| 21 |
+
(`shasum -a 256`). Not uploaded: `__pycache__/`, `.DS_Store`.
|
| 22 |
+
|
| 23 |
+
### To add later (the two runs in progress)
|
| 24 |
+
|
| 25 |
+
When the Gemma multi-mode and 2D runs finish, export them the same way and add:
|
| 26 |
+
|
| 27 |
+
| path in the HF repo | source |
|
| 28 |
+
|---|---|
|
| 29 |
+
| `bridges/gemma-4-12b-4bit-mlx-multimode/{bridges.safetensors,config.json}` | the stage-2b Gemma checkpoint (`results/stage2b_gemma12b*/bridges.npz` + a `config.json` in the format of the 1D one) |
|
| 30 |
+
| `physics/fno_burgers_multimode.safetensors` | `results/hf_export/physics/fno_burgers_multimode.safetensors` (loads with `load_fno_safetensors`, verified) |
|
| 31 |
+
| `bridges/gemma-4-12b-4bit-mlx-2d-dpot/{bridges.safetensors,config.json}` | the stage-2d Gemma checkpoint |
|
| 32 |
+
| `physics/dpot_tiny_fisher2d_finetuned.safetensors` | `results/hf_export/physics/dpot_tiny_fisher2d_finetuned.safetensors` |
|
| 33 |
+
|
| 34 |
+
and fill the two *in progress* rows of `README.md` ("Bridges in this repository") from their
|
| 35 |
+
`final_eval.json`; `psilm_infer.py` will need a `--task` switch for those (it is 1D-only today).
|
| 36 |
+
|
| 37 |
+
## Upload (maintainer runs this)
|
| 38 |
+
|
| 39 |
+
From the GitHub checkout root, with `HF_TOKEN` set (or `huggingface-cli login` done):
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
cd /Users/rxiii/Documents/GitHub/PsiLM
|
| 43 |
+
HF_HUB_DISABLE_XET=1 .venv/bin/python - <<'EOF'
|
| 44 |
+
from huggingface_hub import HfApi
|
| 45 |
+
api = HfApi()
|
| 46 |
+
repo = "ryoji-info/Gemma-4-12B-PsiLM"
|
| 47 |
+
api.create_repo(repo, repo_type="model", private=True, exist_ok=True)
|
| 48 |
+
api.upload_folder(
|
| 49 |
+
folder_path="release/gemma-4-12b-psilm",
|
| 50 |
+
repo_id=repo,
|
| 51 |
+
repo_type="model",
|
| 52 |
+
commit_message="Gemma-4-12B-PsiLM: bridges, Burgers FNO, one-command CLI, model card",
|
| 53 |
+
ignore_patterns=["__pycache__/*", "*.pyc", ".DS_Store"],
|
| 54 |
+
)
|
| 55 |
+
print("UPLOAD DONE")
|
| 56 |
+
EOF
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
`HF_HUB_DISABLE_XET=1` keeps the upload on the classic LFS path (the Xet backend has
|
| 60 |
+
stalled on this machine's earlier uploads). Make the repo public afterwards with
|
| 61 |
+
`api.update_repo_settings(repo, private=False)` or from the repo's settings page, together
|
| 62 |
+
with `ryoji-info/PsiLM-bridges` and `ryoji-info/PsiLM-physics`, which the card links to.
|
| 63 |
+
|
| 64 |
+
## After upload: the one-command check
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
huggingface-cli download ryoji-info/Gemma-4-12B-PsiLM --local-dir /tmp/g4psilm && cd /tmp/g4psilm
|
| 68 |
+
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
|
| 69 |
+
.venv/bin/python psilm_infer.py
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Note for the `pip install` line: the `psilm @ git+...` requirement needs the GitHub
|
| 73 |
+
`pyproject.toml` to use package discovery (`[tool.setuptools.packages.find] include = ["psilm*"]`,
|
| 74 |
+
changed on 2026-09-06 in the working tree from `packages = ["psilm"]`, which shipped only the
|
| 75 |
+
top-level package without `psilm.mlx`, `psilm.stage2`, `psilm.physics`). That change must be
|
| 76 |
+
pushed before the requirement installs a usable package; until then the script says so, and the
|
| 77 |
+
`PSILM_REPO=/path/to/clone` fallback works regardless.
|
README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: mlx-community/gemma-4-12B-it-4bit
|
| 4 |
+
library_name: mlx
|
| 5 |
+
tags:
|
| 6 |
+
- mlx
|
| 7 |
+
- psilm
|
| 8 |
+
- physics
|
| 9 |
+
- gemma
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+

|
| 13 |
+
|
| 14 |
+
# Gemma-4-12B-PsiLM
|
| 15 |
+
|
| 16 |
+
**Gemma 4 12B, coupled to a physics model through trained latent bridges, runnable on a Mac with one command.**
|
| 17 |
+
|
| 18 |
+
*Research generated by Claude Fable 5 (Anthropic) under the direction of Ryoji Furui; see the [AI generation disclosure](#ai-generation-disclosure).*
|
| 19 |
+
|
| 20 |
+
## What this is
|
| 21 |
+
|
| 22 |
+
PsiLM (ΨLM) runs a **frozen language model** and a **frozen physics model** *together* while one answer is produced. Nothing is fine-tuned and no text crosses the interface: small trainable **bridges** read the physical problem out of the language model's hidden states, the physics model computes, and its result flows back into the language model's residual stream as a few soft tokens through a gated cross-attention. This repository packages that system for one backbone:
|
| 23 |
+
|
| 24 |
+
| part | what | trained? | where it comes from |
|
| 25 |
+
|---|---|---|---|
|
| 26 |
+
| language model | Gemma 4 12B-it, 4-bit MLX quantization (48 layers, hidden 3840) | frozen | downloaded from [`mlx-community/gemma-4-12B-it-4bit`](https://huggingface.co/mlx-community/gemma-4-12B-it-4bit) at first run (not redistributed here) |
|
| 27 |
+
| physics model | a 1D Burgers Fourier Neural Operator, 70K parameters (`physics/fno_burgers_singlemode.safetensors`) | frozen | this repo (also in [`ryoji-info/PsiLM-physics`](https://huggingface.co/ryoji-info/PsiLM-physics)) |
|
| 28 |
+
| bridges | forward readout + value-token channel + gated injection, 25.5M parameters (`bridges/gemma-4-12b-4bit-mlx-1d-value-selective/`) | **trained** | this repo (also in [`ryoji-info/PsiLM-bridges`](https://huggingface.co/ryoji-info/PsiLM-bridges)) |
|
| 29 |
+
|
| 30 |
+
**What it can answer today.** One family of questions, the one the bridges were trained on:
|
| 31 |
+
|
| 32 |
+
> A velocity field on the periodic domain [0,1) starts as u(x,0) = *a* · sin(2πx + *φ*). It evolves by Burgers' equation with viscosity 0.02 until t = 0.5. What is the value of u at x = *x₀*? Answer with a number rounded to 2 decimal places.
|
| 33 |
+
|
| 34 |
+
with *a* in [0.5, 1.5], *φ* in [0, 6.28], *x₀* in [0, 0.99], two decimals each. Gemma alone cannot answer these (0% on the held-out set; it derives for 768 tokens and never commits to a number). Coupled through the bridges it answers **96.7%** of them within ±0.05, against **98.3%** when the true value is written into the prompt as text (the oracle ceiling). On everything else the gate stays shut and the model is Gemma, byte for byte in the arms measured (GSM8K 84/84, see below).
|
| 35 |
+
|
| 36 |
+
**What it is not.** Not a general physics assistant, not a fine-tuned Gemma, and not a model that will know when a *different* PDE applies: the physics model solves exactly one equation family and the bridges read exactly three quantities (*a*, *φ*, *x₀*) from the text. It is a research artifact: a working, measured instance of latent coupling between a language model and a physics model at 12B scale, on consumer hardware.
|
| 37 |
+
|
| 38 |
+
## Run it
|
| 39 |
+
|
| 40 |
+
Apple Silicon Mac (the 4-bit backbone is 6.3 GB on disk; training of the bridges peaked at 13 GB on a 24 GB M2, inference needs less), Python 3.11+.
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
# 1. get this repo (≈100 MB of bridges + 0.5 MB of physics model)
|
| 44 |
+
huggingface-cli download ryoji-info/Gemma-4-12B-PsiLM --local-dir Gemma-4-12B-PsiLM
|
| 45 |
+
cd Gemma-4-12B-PsiLM
|
| 46 |
+
|
| 47 |
+
# 2. dependencies (mlx, mlx-lm, transformers, torch, huggingface_hub + the psilm package from GitHub)
|
| 48 |
+
pip install -r requirements.txt
|
| 49 |
+
|
| 50 |
+
# 3. one command
|
| 51 |
+
python psilm_infer.py
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
The first run downloads the Gemma 4 backbone (6.3 GB) into the Hugging Face cache. The script answers the default question (*a* = 1.28, *φ* = 0.5, *x₀* = 0.76) three ways and prints timing:
|
| 55 |
+
|
| 56 |
+
```
|
| 57 |
+
[1] PsiLM (coupled) : the coupled system's reply ('u at x = 0.76 equals <number>.') -- seconds
|
| 58 |
+
[2] backbone alone : Gemma 4 alone under the "Answer: <number>" protocol, answer-forced -- about 75 s
|
| 59 |
+
[3] physics model (FNO) : u(0.76) = -0.2522 (the FNO on the true initial condition: the reference [1] should match to ±0.05)
|
| 60 |
+
spectral solver : u(0.76) = -0.2517 (ground truth)
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Other questions and options:
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
python psilm_infer.py --a 0.9 --phi 2.1 --x0 0.33 # any (a, phi, x0) in the ranges above
|
| 67 |
+
python psilm_infer.py --no-baseline # skip the slow backbone-alone arm
|
| 68 |
+
python psilm_infer.py --question-only # print the exact prompt, load nothing
|
| 69 |
+
python psilm_infer.py --help # --bridges DIR, --physics FILE, --backbone ID, ...
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Without `pip install`-ing the package, a clone of the GitHub repository works too: `PSILM_REPO=/path/to/PsiLM python psilm_infer.py`.
|
| 73 |
+
|
| 74 |
+
Loading the pieces yourself, in Python:
|
| 75 |
+
|
| 76 |
+
```python
|
| 77 |
+
import json, mlx.core as mx
|
| 78 |
+
from psilm.mlx.gemma_loader import load_backbone_any # Gemma 4 text tower in the staged-forward layout
|
| 79 |
+
from psilm.mlx.bridges import PsiBridgesMLX
|
| 80 |
+
from psilm.mlx.fno import load_fno_safetensors
|
| 81 |
+
from psilm.mlx.model import PsiLMMLX
|
| 82 |
+
|
| 83 |
+
model, stock, tok = load_backbone_any("mlx-community/gemma-4-12B-it-4bit")
|
| 84 |
+
d = "bridges/gemma-4-12b-4bit-mlx-1d-value-selective"
|
| 85 |
+
cfg = json.load(open(f"{d}/config.json"))
|
| 86 |
+
bridges = PsiBridgesMLX(**cfg["construct"]) # d_model 3840, channel "value", inj_cap 0.2, readout_norm "dim"
|
| 87 |
+
bridges.load_weights(f"{d}/bridges.safetensors", strict=False) # the retired learned-pointer tensors are omitted
|
| 88 |
+
fno = load_fno_safetensors("physics/fno_burgers_singlemode.safetensors")
|
| 89 |
+
psi = PsiLMMLX(model, tok, fno, bridges, l_fwd=cfg["coupling"]["l_fwd"], l_rev=cfg["coupling"]["l_rev"]) # read @20, inject @30 of 48
|
| 90 |
+
# psi.generate(QABuilder(hf_tokenizer), {"a": 1.28, "phi": 0.5, "x0": 0.76}) -- see psilm_infer.py
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
## Results for this backbone
|
| 94 |
+
|
| 95 |
+
Held-out evaluation, 60 questions, accuracy within ±0.05 (`results/stage2_gemma12b/final_eval.json` in the [GitHub repository](https://github.com/ryoji-info/PsiLM)):
|
| 96 |
+
|
| 97 |
+
| arm | accuracy | MAE | note |
|
| 98 |
+
|---|---:|---:|---|
|
| 99 |
+
| Gemma 4 12B alone | 0.0% | 2.93 | never reaches an `Answer:` line within 768 tokens |
|
| 100 |
+
| **PsiLM (this repo)** | **96.7%** | **0.017** | bridges read the prompt, FNO computes, value returns in latent space |
|
| 101 |
+
| oracle (true value written into the prompt) | 98.3% | 0.007 | the tool-loop ceiling |
|
| 102 |
+
| always answer 0.00 | 1.7% | 0.308 | calibration |
|
| 103 |
+
|
| 104 |
+
Guard-rail: does the coupled model still do everything else? 100 questions per dataset, three arms — backbone alone / PsiLM / PsiLM with the injection zeroed — with the gate recorded per question (`results/bench/gemma12b_guardrail_summary.json`, `results/bench/gemma12b_nonudge_guardrail_summary.json`):
|
| 105 |
+
|
| 106 |
+
| dataset (n=100) | backbone | **PsiLM** | zeroed | gate σ (PsiLM) | open on |
|
| 107 |
+
|---|---:|---:|---:|---:|---:|
|
| 108 |
+
| physics QA (this task) | 0% | **97%** | 10% | 0.14 | 100% |
|
| 109 |
+
| GSM8K | 84% | **84%** | 84% | 0.004 | 0% |
|
| 110 |
+
| GSM8K, no `Answer:` line in the prompt | 83% | **83%** | 83% | 0.002 | 0% |
|
| 111 |
+
| MMLU, 5 subjects, 256 tokens | 53% | **55%** | 53% | 0.008 | 0% |
|
| 112 |
+
|
| 113 |
+
On MMLU the two arms agree at 79.1% / 79.1% on the 67 items both answer within the budget; the raw 53 vs 55 is parse noise. The gate selectivity comes from a **no-harm training arm**: 1,046 non-physics prompts (GSM8K train, MMLU validation, with and without the `Answer:` nudge) paired with Gemma's own greedy continuations, on which only the gate is updated with a mean-gate penalty (`config.json` → `training`). The 10% of the zeroed arm on physics is what the reply template alone recovers; the coupled 97% is against that floor.
|
| 114 |
+
|
| 115 |
+
Training: 7,000 steps at batch 4 on one Apple M2 (24 GB) — 2,000 readout-only warm-up steps, 3,500 coupled steps (lr 3e-4; the coupled phase ran on to step 6,000 but the no-harm phase resumed from the step-5,500 checkpoint), then 1,500 no-harm steps at lr 1e-4 (`results/gemma12b/noharm_recipe.sh`); 12 s per step at a 13 GB peak. One backbone-specific adjustment, measured rather than tuned: Gemma's massive-activation dimensions are nearly constant across prompts, so the readout standardizes each hidden dimension with statistics from a 32-prompt calibration pass (`readout_norm: "dim"`; the two frozen vectors `fwd.dim_mu`, `fwd.dim_sigma` are in the checkpoint).
|
| 116 |
+
|
| 117 |
+
### Bridges in this repository
|
| 118 |
+
|
| 119 |
+
| directory | task | physics model | trained params | held-out | status |
|
| 120 |
+
|---|---|---|---|---:|---|
|
| 121 |
+
| `bridges/gemma-4-12b-4bit-mlx-1d-value-selective/` | 1D Burgers, single-mode initial conditions, value at *x₀* | `physics/fno_burgers_singlemode.safetensors` | 25.5M | **96.7%** @±0.05, MAE 0.017 (n=60); GSM8K 84/84 | **released** |
|
| 122 |
+
| `bridges/gemma-4-12b-4bit-mlx-multimode/` | 1D Burgers, multi-mode initial conditions (modes 1–4, mixed amplitudes) + generalization families | `physics/fno_burgers_multimode.safetensors` | — | *in progress* | run in progress; row filled when the evaluation lands |
|
| 123 |
+
| `bridges/gemma-4-12b-4bit-mlx-2d-dpot/` | 2D Fisher–KPP, replicated-IC history → u(0.4), value at (x₀, y₀) | `physics/dpot_tiny_fisher2d_finetuned.safetensors` (DPOT-Tiny, 7.5M, fine-tuned) | — | *in progress* | run in progress; row filled when the evaluation lands |
|
| 124 |
+
|
| 125 |
+
The two in-progress rows are the multi-mode (stage 2b) and 2D (stage 2d) tasks that the 0.5B backbone completed earlier (iid 97.9% and 95.0% respectively, see [`ryoji-info/PsiLM-bridges`](https://huggingface.co/ryoji-info/PsiLM-bridges)); their Gemma runs are training now and will be added with their own `config.json`, the matching physics file, and a `--task` switch in `psilm_infer.py`.
|
| 126 |
+
|
| 127 |
+
## How it works, in one paragraph
|
| 128 |
+
|
| 129 |
+
The prompt runs through Gemma's first 20 layers. The **forward bridge** reads the queried position *x₀* by pooling the hidden states over its tokens (a deterministic span pointer computed by the QA builder, plus a 100-bin classifier) and the initial-condition parameters with a learned pool, after the calibrated per-dimension standardization) and emits (*a*, sin *φ*, cos *φ*) and *x₀*; from these it builds the initial condition on a 128-point grid. The frozen **FNO** evolves it to t = 0.5. A learned periodic lookup kernel reads the field at *x₀*, and the **value-token channel** turns that single number into eight soft tokens through Fourier features. At layer 30 a **gated cross-attention** injects them into the residual stream, capped at 20% of the stream's RMS; the gate is a small MLP on the residual stream, trained to open on physics prompts and close elsewhere. Layers 30–48 and the answer are Gemma's own. Details, ablations and the failure analysis that produced this design are in the paper (`paper/psilm.pdf` in the repository, Section 9 for scaling, the guard-rail and Gemma).
|
| 130 |
+
|
| 131 |
+
## Limitations
|
| 132 |
+
|
| 133 |
+
- **One task family.** The bridges read exactly the three quantities of the trained question and the FNO solves exactly one equation family; a different PDE, boundary condition, viscosity or final time is out of scope, and the gate closing on non-physics text does not mean it can recognize *other* physics. Free-text initial conditions ("a Gaussian bump near the left edge") are not supported.
|
| 134 |
+
- **The pointer is task-supplied.** Which tokens hold *x₀* is computed by the QA builder from the prompt (`QABuilder.x0_span`), not learned from the words; the learned attention pointer never left uniform at 8B and was retired. `psilm_infer.py` builds the prompt itself for that reason; a paraphrased question is not the trained input.
|
| 135 |
+
- **4-bit backbone, quantized kernels.** The bridges were trained *through* the 4-bit backbone and absorb its quantization noise, but MLX's quantized matmuls are not guaranteed bit-identical across `mlx` versions or Apple chips, so a given question can land a hundredth away from the recorded run; the numbers above are from mlx 0.32.2 / mlx-lm 0.31.3 on an M2.
|
| 136 |
+
- **Inputs with two decimals, inside the training ranges.** The readout was trained on numbers formatted like the training set; `psilm_infer.py` rounds inputs to two decimals and warns outside [0.5, 1.5] × [0, 6.28].
|
| 137 |
+
- **Bridges do not transfer between backbones.** This checkpoint is for `mlx-community/gemma-4-12B-it-4bit` exactly (hidden 3840, 48 layers); the script refuses a backbone of another width.
|
| 138 |
+
- **Sequence length.** The Gemma loader drives the sliding-window layers with a plain causal mask, exact up to the 1024-token window; the questions here are ~120 tokens.
|
| 139 |
+
- **Evaluation scope.** The guard-rail covers GSM8K, a five-subject MMLU slice and this physics set at n=100 each; nothing else has been measured.
|
| 140 |
+
|
| 141 |
+
## Beyond physics
|
| 142 |
+
|
| 143 |
+
The bridges here couple a frozen language model to a frozen *physics* model, but the recipe (read a fixed set of quantities from text; let a frozen quantitative model compute; return one value through a selective gate) is not specific to PDEs. A calibrated market or event-probability model in the physics model's seat would be the same architecture, and the appeal is the same: a language model's forecast grounded in a model that can be validated separately, with a gate that stays shut when the model does not apply. Nothing in this repository has been trained or tested on financial data; the physics results relied on exact oracles, deterministic targets and no distribution shift, none of which markets provide. This is a research direction, not a capability, and not a basis for investment decisions.
|
| 144 |
+
|
| 145 |
+
## Files
|
| 146 |
+
|
| 147 |
+
```
|
| 148 |
+
psilm_infer.py the one-command CLI (PsiLM / backbone alone / physics model)
|
| 149 |
+
requirements.txt pip dependencies, including the psilm package from GitHub
|
| 150 |
+
bridges/gemma-4-12b-4bit-mlx-1d-value-selective/
|
| 151 |
+
bridges.safetensors the trained bridges (25.5M params, fp32, 97 MB)
|
| 152 |
+
config.json construction, coupling depths, training record, per-chunk held-out scores
|
| 153 |
+
physics/fno_burgers_singlemode.safetensors the frozen FNO (torch key names; complex spectral weights as .real/.imag)
|
| 154 |
+
psilm-banner.png banner
|
| 155 |
+
README.md this card
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## Related
|
| 159 |
+
|
| 160 |
+
- Code, training scripts, evaluation records and the paper: [github.com/ryoji-info/PsiLM](https://github.com/ryoji-info/PsiLM)
|
| 161 |
+
- Bridges for every backbone (Qwen2.5-0.5B, Qwen3-1.7B, Qwen3-8B, Gemma 4 12B): [ryoji-info/PsiLM-bridges](https://huggingface.co/ryoji-info/PsiLM-bridges)
|
| 162 |
+
- The frozen physics models: [ryoji-info/PsiLM-physics](https://huggingface.co/ryoji-info/PsiLM-physics)
|
| 163 |
+
|
| 164 |
+
## Citation
|
| 165 |
+
|
| 166 |
+
```bibtex
|
| 167 |
+
@misc{furui2026psilm,
|
| 168 |
+
title = {PsiLM: Coupling Frozen Language and Physics Models through Trainable Latent Bridges},
|
| 169 |
+
author = {Furui, Ryoji},
|
| 170 |
+
year = {2026},
|
| 171 |
+
url = {https://github.com/ryoji-info/PsiLM},
|
| 172 |
+
note = {Research generated by Claude Fable 5 (Anthropic) under the author's direction}
|
| 173 |
+
}
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
## License
|
| 177 |
+
|
| 178 |
+
The bridges, the FNO and the code in this repository are released under **Apache-2.0**. The Gemma 4 backbone is not part of this repository: `psilm_infer.py` downloads it from `mlx-community/gemma-4-12B-it-4bit`, and its use is governed by Google's [Gemma Terms of Use](https://ai.google.dev/gemma/terms).
|
| 179 |
+
|
| 180 |
+
## AI generation disclosure
|
| 181 |
+
|
| 182 |
+
This model, its training recipe, the evaluations and this card were generated by **Claude Fable 5** (Anthropic), operating as an autonomous research agent under the direction and review of Ryoji Furui, who set the research question and the hardware constraint, approved each stage, and bears responsibility for the published claims. All numbers on this card are taken from committed evaluation records in the repository, cited by file name above.
|
bridges/gemma-4-12b-4bit-mlx-1d-value-selective/bridges.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f6ef8946c41b3cfa17df7c22bcab2c5cffbd856c2dd608970a364cc14d5e8f7d
|
| 3 |
+
size 102068660
|
bridges/gemma-4-12b-4bit-mlx-1d-value-selective/config.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backbone": "mlx-community/gemma-4-12B-it-4bit",
|
| 3 |
+
"hf_tokenizer": "mlx-community/gemma-4-12B-it-4bit",
|
| 4 |
+
"loader": "psilm.mlx.gemma_loader.load_backbone_any (gemma4_unified -> text tower)",
|
| 5 |
+
"physics": "results/stage2/fno.pt (1D Burgers FNO, 70K params)",
|
| 6 |
+
"bridges_class": "psilm.mlx.bridges.PsiBridgesMLX",
|
| 7 |
+
"construct": {
|
| 8 |
+
"d_model": 3840,
|
| 9 |
+
"channel": "value",
|
| 10 |
+
"inj_cap": 0.2,
|
| 11 |
+
"gate_bias": 0.0,
|
| 12 |
+
"readout_norm": "dim"
|
| 13 |
+
},
|
| 14 |
+
"coupling": {
|
| 15 |
+
"l_fwd": 20,
|
| 16 |
+
"l_rev": 30,
|
| 17 |
+
"n_layers": 48
|
| 18 |
+
},
|
| 19 |
+
"pointer": "deterministic span pooling with calibrated per-dimension standardization (fwd.dim_mu/dim_sigma included; fwd.x0_query/x0_key omitted: unused)",
|
| 20 |
+
"training": {
|
| 21 |
+
"steps_total": 7000,
|
| 22 |
+
"phase_A_readout_only": 2000,
|
| 23 |
+
"coupled_steps": 4000,
|
| 24 |
+
"no_harm_steps": 1500,
|
| 25 |
+
"batch": 4,
|
| 26 |
+
"lr": "3e-4 (warm-up, coupled), 1e-4 (no-harm phase, from the step-5500 checkpoint)",
|
| 27 |
+
"no_harm_arm": "1046 non-physics prompts (GSM8K train, MMLU validation; with/without the Answer nudge) paired with the backbone's own greedy continuation; gate-only updates + mean-gate penalty"
|
| 28 |
+
},
|
| 29 |
+
"held_out_n48_per_chunk": {
|
| 30 |
+
"coupled": [
|
| 31 |
+
0.625,
|
| 32 |
+
0.667,
|
| 33 |
+
0.625,
|
| 34 |
+
0.875,
|
| 35 |
+
0.875,
|
| 36 |
+
0.625,
|
| 37 |
+
0.792,
|
| 38 |
+
0.542
|
| 39 |
+
],
|
| 40 |
+
"no_harm_phase": [
|
| 41 |
+
1.0,
|
| 42 |
+
1.0,
|
| 43 |
+
0.979
|
| 44 |
+
]
|
| 45 |
+
},
|
| 46 |
+
"license": "apache-2.0"
|
| 47 |
+
}
|
physics/fno_burgers_singlemode.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7bb0076c85cdcf2505a9079c05964e3eb77ac4a776eccf34216953f3c37bfcdd
|
| 3 |
+
size 552076
|
psilm-banner.png
ADDED
|
Git LFS Details
|
psilm_infer.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""PsiLM on Gemma 4 12B: one command, three numbers.
|
| 3 |
+
|
| 4 |
+
python psilm_infer.py # a=1.28, phi=0.5, x0=0.76
|
| 5 |
+
python psilm_infer.py --a 0.9 --phi 2.1 --x0 0.33
|
| 6 |
+
python psilm_infer.py --question-only # print the prompt, load nothing
|
| 7 |
+
python psilm_infer.py --no-baseline # skip the (slow) backbone-alone arm
|
| 8 |
+
|
| 9 |
+
Loads a frozen 4-bit language model (default ``mlx-community/gemma-4-12B-it-4bit``),
|
| 10 |
+
the trained PsiLM bridges for it (``bridges/<name>/{bridges.safetensors,config.json}``)
|
| 11 |
+
and the frozen 1D Burgers FNO (``physics/fno_burgers_singlemode.safetensors``), then
|
| 12 |
+
answers one Burgers field-value question three ways:
|
| 13 |
+
|
| 14 |
+
PsiLM the coupled system: the forward bridge reads the initial condition and
|
| 15 |
+
the queried position x0 out of the prompt's hidden states, the FNO evolves
|
| 16 |
+
the field, and the looked-up value u(x0) returns to the language model as
|
| 17 |
+
soft tokens through a gated cross-attention. No text crosses the interface.
|
| 18 |
+
backbone the same language model alone, "Answer: <number>" protocol with answer
|
| 19 |
+
forcing (the baseline arm of eval/mlx_stage2_eval.py in the repository).
|
| 20 |
+
physics the FNO's own value at x0 on the TRUE initial condition -- the reference
|
| 21 |
+
the coupled answer should match to +-0.05 -- plus the spectral solver's
|
| 22 |
+
ground truth for the same question.
|
| 23 |
+
|
| 24 |
+
Dependencies: ``pip install -r requirements.txt`` (mlx, mlx-lm, transformers, torch,
|
| 25 |
+
huggingface_hub, and the ``psilm`` package from https://github.com/ryoji-info/PsiLM).
|
| 26 |
+
If ``psilm`` is not installed, set ``PSILM_REPO=/path/to/PsiLM`` (a clone) instead.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
import argparse
|
| 30 |
+
import importlib
|
| 31 |
+
import importlib.util
|
| 32 |
+
import json
|
| 33 |
+
import math
|
| 34 |
+
import os
|
| 35 |
+
import re
|
| 36 |
+
import sys
|
| 37 |
+
import time
|
| 38 |
+
from pathlib import Path
|
| 39 |
+
|
| 40 |
+
HERE = Path(__file__).resolve().parent
|
| 41 |
+
DEFAULT_BACKBONE = "mlx-community/gemma-4-12B-it-4bit"
|
| 42 |
+
DEFAULT_BRIDGES = HERE / "bridges" / "gemma-4-12b-4bit-mlx-1d-value-selective"
|
| 43 |
+
DEFAULT_PHYSICS = HERE / "physics" / "fno_burgers_singlemode.safetensors"
|
| 44 |
+
TOL = 0.05
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# --------------------------------------------------------------------------- imports
|
| 48 |
+
def _psilm_available():
|
| 49 |
+
try:
|
| 50 |
+
return importlib.util.find_spec("psilm.mlx") is not None
|
| 51 |
+
except ModuleNotFoundError:
|
| 52 |
+
return False
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _ensure_psilm():
|
| 56 |
+
"""Import path for the ``psilm`` package: the installed package first, else the
|
| 57 |
+
optional ``PSILM_REPO`` environment variable (a clone of the GitHub repository)."""
|
| 58 |
+
if _psilm_available():
|
| 59 |
+
return None
|
| 60 |
+
repo = os.environ.get("PSILM_REPO")
|
| 61 |
+
if repo and (Path(repo) / "psilm" / "mlx").is_dir():
|
| 62 |
+
sys.path.insert(0, str(Path(repo).resolve()))
|
| 63 |
+
for name in [m for m in sys.modules if m == "psilm" or m.startswith("psilm.")]:
|
| 64 |
+
del sys.modules[name] # a partial install may already be imported
|
| 65 |
+
importlib.invalidate_caches()
|
| 66 |
+
if _psilm_available():
|
| 67 |
+
return str(Path(repo).resolve())
|
| 68 |
+
sys.exit(
|
| 69 |
+
"psilm_infer.py: the 'psilm' package (with its psilm.mlx subpackage) is not importable.\n"
|
| 70 |
+
" Install it: pip install -r requirements.txt\n"
|
| 71 |
+
" (or: pip install 'git+https://github.com/ryoji-info/PsiLM')\n"
|
| 72 |
+
" Or point at a clone: PSILM_REPO=/path/to/PsiLM python psilm_infer.py ..."
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
PSILM_REPO = _ensure_psilm()
|
| 77 |
+
|
| 78 |
+
import mlx.core as mx # noqa: E402
|
| 79 |
+
from mlx.utils import tree_flatten # noqa: E402
|
| 80 |
+
import mlx_lm # noqa: E402
|
| 81 |
+
import numpy as np # noqa: E402
|
| 82 |
+
from transformers import AutoTokenizer # noqa: E402
|
| 83 |
+
|
| 84 |
+
from psilm.mlx.bridges import PsiBridgesMLX, build_ic_mlx # noqa: E402
|
| 85 |
+
from psilm.mlx.fno import convert_from_torch, load_fno_safetensors # noqa: E402
|
| 86 |
+
from psilm.mlx.gemma_loader import load_backbone_any # noqa: E402
|
| 87 |
+
from psilm.mlx.model import PsiLMMLX # noqa: E402
|
| 88 |
+
from psilm.physics.burgers import initial_condition, solve # noqa: E402
|
| 89 |
+
from psilm.stage2.qa import QUESTION, SYSTEM, QABuilder, fourier_interp # noqa: E402
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# --------------------------------------------------------------- backbone-alone arm
|
| 93 |
+
# The baseline protocol is eval/mlx_stage2_eval.py's: the question plus the "Answer:"
|
| 94 |
+
# nudge, greedy generation, and answer forcing when the reply used its budget without
|
| 95 |
+
# an Answer line. When a repository clone is reachable (PSILM_REPO) the functions are
|
| 96 |
+
# imported from that file so the release cannot drift from the evaluation; otherwise
|
| 97 |
+
# the verbatim copies below are used.
|
| 98 |
+
NUDGE = "\nEnd your reply with a line of the form \"Answer: <number>\"."
|
| 99 |
+
FORCE_SUFFIX = "\n\nAnswer:"
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def parse_value(text):
|
| 103 |
+
m = re.findall(r"Answer:\s*\$?\\?\(?\s*(-?\d+\.?\d*)", text)
|
| 104 |
+
if m:
|
| 105 |
+
return float(m[-1])
|
| 106 |
+
m = re.findall(r"-?\d+\.\d+", text)
|
| 107 |
+
return float(m[-1]) if m else None
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def chat_generate(model, hf_tok, user, max_new=768, gen_tok=None, force_answer=True):
|
| 111 |
+
"""Returns (text, forced). gen_tok is the mlx-lm tokenizer wrapper (knows all of
|
| 112 |
+
a backbone's stop ids, e.g. Gemma's <eos>/<turn|>); hf_tok builds the prompt."""
|
| 113 |
+
messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": user}]
|
| 114 |
+
ids = hf_tok.apply_chat_template(messages, tokenize=True, add_generation_prompt=True,
|
| 115 |
+
enable_thinking=False)
|
| 116 |
+
if not isinstance(ids, list):
|
| 117 |
+
ids = ids["input_ids"]
|
| 118 |
+
if ids and isinstance(ids[0], list):
|
| 119 |
+
ids = ids[0]
|
| 120 |
+
text = mlx_lm.generate(model, gen_tok or hf_tok, prompt=list(ids), max_tokens=max_new, verbose=False)
|
| 121 |
+
if force_answer and "Answer:" not in text:
|
| 122 |
+
cont = list(ids) + hf_tok.encode(text + FORCE_SUFFIX, add_special_tokens=False)
|
| 123 |
+
tail = mlx_lm.generate(model, gen_tok or hf_tok, prompt=cont, max_tokens=16, verbose=False)
|
| 124 |
+
return text + FORCE_SUFFIX + tail, True
|
| 125 |
+
return text, False
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _baseline_protocol():
|
| 129 |
+
"""(chat_generate, parse_value, NUDGE, source): the repository's eval functions
|
| 130 |
+
when a clone is reachable, else the copies in this file."""
|
| 131 |
+
roots = [Path(PSILM_REPO)] if PSILM_REPO else []
|
| 132 |
+
for root in roots:
|
| 133 |
+
f = root / "eval" / "mlx_stage2_eval.py"
|
| 134 |
+
if f.is_file():
|
| 135 |
+
spec = importlib.util.spec_from_file_location("psilm_release_eval_stage2", f)
|
| 136 |
+
mod = importlib.util.module_from_spec(spec)
|
| 137 |
+
spec.loader.exec_module(mod)
|
| 138 |
+
return mod.chat_generate, mod.parse_value, mod.NUDGE, str(f)
|
| 139 |
+
return chat_generate, parse_value, NUDGE, "vendored copy (eval/mlx_stage2_eval.py)"
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def parse_psilm_answer(text):
|
| 143 |
+
"""The trained reply is 'u at x = {x0} equals {u}.'; the number after 'equals' is
|
| 144 |
+
the answer (the evaluation scores only that). Returns (value, strict): strict is
|
| 145 |
+
False when the reply left the template and the last decimal number is used."""
|
| 146 |
+
m = re.search(r"equals\s*(-?\d+\.?\d*)", text)
|
| 147 |
+
if m:
|
| 148 |
+
return float(m.group(1)), True
|
| 149 |
+
m = re.findall(r"-?\d+\.\d+", text)
|
| 150 |
+
return (float(m[-1]), False) if m else (None, False)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# ------------------------------------------------------------------------ bridges
|
| 154 |
+
def load_bridges(bridges_dir, d_model, n_layers, args):
|
| 155 |
+
"""PsiBridgesMLX from <dir>/config.json['construct'] (defaults when the file is
|
| 156 |
+
absent, inferred from the tensors present) + <dir>/bridges.safetensors with
|
| 157 |
+
load_weights(strict=False): the Gemma export omits the retired learned-pointer
|
| 158 |
+
tensors (fwd.x0_query, fwd.x0_key.*), which the deterministic span pointer never
|
| 159 |
+
uses. Shapes of every provided tensor are checked explicitly, since non-strict
|
| 160 |
+
loading would not. Returns (bridges, config, coupling, report)."""
|
| 161 |
+
bridges_dir = Path(bridges_dir)
|
| 162 |
+
cfg_path = bridges_dir / "config.json"
|
| 163 |
+
cfg = json.loads(cfg_path.read_text()) if cfg_path.is_file() else {}
|
| 164 |
+
weights = mx.load(str(bridges_dir / "bridges.safetensors"))
|
| 165 |
+
keys = set(weights)
|
| 166 |
+
|
| 167 |
+
construct = {
|
| 168 |
+
"d_model": int(weights["fwd.query"].shape[0]) if "fwd.query" in keys else d_model,
|
| 169 |
+
"channel": "value" if any(k.startswith("val.") for k in keys) else "field",
|
| 170 |
+
"gate_bias": -2.0,
|
| 171 |
+
"inj_cap": None,
|
| 172 |
+
"readout_norm": "dim" if "fwd.dim_mu" in keys else "rms",
|
| 173 |
+
}
|
| 174 |
+
construct.update(cfg.get("construct", {}))
|
| 175 |
+
for name in ("channel", "gate_bias", "inj_cap", "readout_norm"): # CLI overrides
|
| 176 |
+
v = getattr(args, name)
|
| 177 |
+
if v is not None:
|
| 178 |
+
construct[name] = v
|
| 179 |
+
if int(construct["d_model"]) != int(d_model):
|
| 180 |
+
sys.exit(f"bridges were trained for hidden size {construct['d_model']} but the "
|
| 181 |
+
f"backbone has {d_model}: bridges do not transfer between backbones")
|
| 182 |
+
bridges = PsiBridgesMLX(**construct)
|
| 183 |
+
|
| 184 |
+
# explicit shape check + missing/unexpected report
|
| 185 |
+
params = dict(tree_flatten(bridges.parameters()))
|
| 186 |
+
missing = sorted(k for k in params if k not in keys)
|
| 187 |
+
unexpected = sorted(k for k in keys if k not in params)
|
| 188 |
+
bad = [(k, tuple(weights[k].shape), tuple(params[k].shape))
|
| 189 |
+
for k in keys if k in params and tuple(weights[k].shape) != tuple(params[k].shape)]
|
| 190 |
+
if bad:
|
| 191 |
+
lines = "\n".join(f" {k}: file {a} vs module {b}" for k, a, b in bad)
|
| 192 |
+
sys.exit(f"bridge tensor shapes do not match the constructed bridges:\n{lines}")
|
| 193 |
+
if unexpected:
|
| 194 |
+
sys.exit(f"bridges.safetensors has tensors the bridges do not define: {unexpected}")
|
| 195 |
+
bridges.load_weights(list(weights.items()), strict=False)
|
| 196 |
+
mx.eval(bridges.parameters())
|
| 197 |
+
|
| 198 |
+
coupling = dict(cfg.get("coupling", {}))
|
| 199 |
+
if args.l_fwd is not None:
|
| 200 |
+
coupling["l_fwd"] = args.l_fwd
|
| 201 |
+
if args.l_rev is not None:
|
| 202 |
+
coupling["l_rev"] = args.l_rev
|
| 203 |
+
if "n_layers" in coupling and int(coupling["n_layers"]) != int(n_layers):
|
| 204 |
+
sys.exit(f"config.json expects a {coupling['n_layers']}-layer backbone; "
|
| 205 |
+
f"this one has {n_layers}")
|
| 206 |
+
n_trained = sum(int(np.prod(v.shape)) for k, v in params.items() if k in keys)
|
| 207 |
+
report = {"construct": construct, "missing": missing, "n_tensors": len(keys),
|
| 208 |
+
"n_params": n_trained}
|
| 209 |
+
return bridges, cfg, coupling, report
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def load_physics(path):
|
| 213 |
+
path = str(path)
|
| 214 |
+
if path.endswith(".pt"):
|
| 215 |
+
return convert_from_torch(path)
|
| 216 |
+
return load_fno_safetensors(path)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# --------------------------------------------------------------------------- main
|
| 220 |
+
def build_parser():
|
| 221 |
+
ap = argparse.ArgumentParser(
|
| 222 |
+
description="PsiLM (frozen LLM + frozen Burgers FNO through latent bridges): "
|
| 223 |
+
"answer one field-value question three ways.",
|
| 224 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 225 |
+
epilog="Training ranges: a in [0.5, 1.5], phi in [0, 6.28], x0 in [0, 0.99], "
|
| 226 |
+
"all with two decimals. Outside them the bridges are extrapolating.")
|
| 227 |
+
ap.add_argument("--a", type=float, default=1.28, help="amplitude of u(x,0) = a sin(2 pi x + phi)")
|
| 228 |
+
ap.add_argument("--phi", type=float, default=0.5, help="phase (radians)")
|
| 229 |
+
ap.add_argument("--x0", type=float, default=0.76, help="queried position in [0, 1)")
|
| 230 |
+
ap.add_argument("--backbone", default=DEFAULT_BACKBONE,
|
| 231 |
+
help="mlx-lm checkpoint (Hugging Face id or local path)")
|
| 232 |
+
ap.add_argument("--bridges", default=str(DEFAULT_BRIDGES),
|
| 233 |
+
help="directory with bridges.safetensors (+ config.json)")
|
| 234 |
+
ap.add_argument("--physics", default=str(DEFAULT_PHYSICS),
|
| 235 |
+
help="FNO weights: safetensors export, or a PyTorch fno.pt")
|
| 236 |
+
ap.add_argument("--hf-tokenizer", default=None,
|
| 237 |
+
help="HF tokenizer id for the chat template (default: config.json's "
|
| 238 |
+
"hf_tokenizer, else the backbone id)")
|
| 239 |
+
ap.add_argument("--no-baseline", action="store_true", help="skip the backbone-alone arm")
|
| 240 |
+
ap.add_argument("--question-only", action="store_true",
|
| 241 |
+
help="print the question the models see and exit (loads nothing)")
|
| 242 |
+
ap.add_argument("--max-new", type=int, default=24, help="PsiLM reply budget (tokens)")
|
| 243 |
+
ap.add_argument("--baseline-max-new", type=int, default=768,
|
| 244 |
+
help="backbone-alone reply budget before answer forcing (the "
|
| 245 |
+
"evaluation used 768; Gemma 4 uses all of it, ~75 s)")
|
| 246 |
+
g = ap.add_argument_group("bridge construction overrides (default: config.json)")
|
| 247 |
+
g.add_argument("--channel", choices=["value", "field"], default=None)
|
| 248 |
+
g.add_argument("--gate-bias", type=float, default=None)
|
| 249 |
+
g.add_argument("--inj-cap", type=float, default=None)
|
| 250 |
+
g.add_argument("--readout-norm", choices=["rms", "dim"], default=None)
|
| 251 |
+
g.add_argument("--l-fwd", type=int, default=None, help="readout layer")
|
| 252 |
+
g.add_argument("--l-rev", type=int, default=None, help="injection layer")
|
| 253 |
+
ap.add_argument("-v", "--verbose", action="store_true")
|
| 254 |
+
return ap
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def _clean(text):
|
| 258 |
+
return re.sub(r"<\|?[a-z_|]+\|?>", "", text).strip()
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def main():
|
| 262 |
+
args = build_parser().parse_args()
|
| 263 |
+
item = {"a": round(args.a, 2), "phi": round(args.phi, 2), "x0": round(args.x0, 2)}
|
| 264 |
+
if (item["a"], item["phi"], item["x0"]) != (args.a, args.phi, args.x0):
|
| 265 |
+
print(f"note: inputs rounded to two decimals (the trained readout reads 2-decimal "
|
| 266 |
+
f"numbers): a={item['a']} phi={item['phi']} x0={item['x0']}")
|
| 267 |
+
if not (0.0 <= item["x0"] < 1.0):
|
| 268 |
+
sys.exit("x0 must lie in [0, 1): the domain is periodic")
|
| 269 |
+
if not (0.5 <= item["a"] <= 1.5 and 0.0 <= item["phi"] <= 6.28):
|
| 270 |
+
print("warning: a or phi lies outside the training ranges (a in [0.5, 1.5], "
|
| 271 |
+
"phi in [0, 6.28]); the bridges are extrapolating")
|
| 272 |
+
question = QUESTION.format(a=item["a"], phi=item["phi"], x0=item["x0"])
|
| 273 |
+
|
| 274 |
+
if args.question_only:
|
| 275 |
+
print(f"[system] {SYSTEM}")
|
| 276 |
+
print(f"[user] {question}")
|
| 277 |
+
print(f"\n(the backbone-alone arm appends: {NUDGE.strip()!r})")
|
| 278 |
+
return
|
| 279 |
+
|
| 280 |
+
# ---- load
|
| 281 |
+
t0 = time.time()
|
| 282 |
+
print(f"backbone : {args.backbone}", flush=True)
|
| 283 |
+
model, stock, tok = load_backbone_any(args.backbone)
|
| 284 |
+
n_layers = len(model.model.layers)
|
| 285 |
+
d_model = int(model.args.hidden_size)
|
| 286 |
+
bridges, cfg, coupling, rep = load_bridges(args.bridges, d_model, n_layers, args)
|
| 287 |
+
hf_id = args.hf_tokenizer or cfg.get("hf_tokenizer") or args.backbone
|
| 288 |
+
hf_tok = AutoTokenizer.from_pretrained(hf_id)
|
| 289 |
+
fno = load_physics(args.physics)
|
| 290 |
+
psi = PsiLMMLX(model, tok, fno, bridges, l_fwd=coupling.get("l_fwd"), l_rev=coupling.get("l_rev"))
|
| 291 |
+
builder = QABuilder(hf_tok)
|
| 292 |
+
t_load = time.time() - t0
|
| 293 |
+
c = rep["construct"]
|
| 294 |
+
print(f"bridges : {args.bridges}\n"
|
| 295 |
+
f" channel={c['channel']} readout_norm={c['readout_norm']} "
|
| 296 |
+
f"gate_bias={c['gate_bias']} inj_cap={c['inj_cap']} | "
|
| 297 |
+
f"{rep['n_params']/1e6:.1f}M params in {rep['n_tensors']} tensors"
|
| 298 |
+
+ (f" | not in file (unused): {rep['missing']}" if rep["missing"] else ""))
|
| 299 |
+
print(f"coupling : read @ layer {psi.l_fwd}, inject @ layer {psi.l_rev} of {psi.n_layers} "
|
| 300 |
+
f"(hidden {d_model}"
|
| 301 |
+
+ (")" if cfg.get("coupling") else "; no config.json coupling entry: PsiLMMLX defaults)"))
|
| 302 |
+
# parameter count with each complex spectral weight counted once (wr/wi are one number)
|
| 303 |
+
n_fno = sum(int(np.prod(v.shape)) for k, v in tree_flatten(fno.parameters()) if not k.endswith(".wi"))
|
| 304 |
+
print(f"physics : {args.physics} (FNO1d, {n_fno/1e3:.0f}K params)")
|
| 305 |
+
print(f"loaded in {t_load:.1f} s\n")
|
| 306 |
+
print(f"question : {question}\n", flush=True)
|
| 307 |
+
|
| 308 |
+
# ---- 1. PsiLM: the coupled system
|
| 309 |
+
t1 = time.time()
|
| 310 |
+
psi_text = psi.generate(builder, item, max_new=args.max_new)
|
| 311 |
+
t_psi = time.time() - t1
|
| 312 |
+
psi_val, strict = parse_psilm_answer(psi_text)
|
| 313 |
+
print(f"[1] PsiLM (coupled) : {_clean(psi_text)!r}")
|
| 314 |
+
print(f" value : {psi_val if psi_val is not None else 'no number parsed'}"
|
| 315 |
+
f" ({t_psi:.1f} s{'' if strict or psi_val is None else '; reply left the trained template, last number taken'})",
|
| 316 |
+
flush=True)
|
| 317 |
+
|
| 318 |
+
# ---- 2. backbone alone
|
| 319 |
+
base_val = None
|
| 320 |
+
if not args.no_baseline:
|
| 321 |
+
gen, parse, nudge, src = _baseline_protocol()
|
| 322 |
+
t2 = time.time()
|
| 323 |
+
base_text, forced = gen(stock, hf_tok, question + nudge, args.baseline_max_new,
|
| 324 |
+
gen_tok=tok, force_answer=True)
|
| 325 |
+
t_base = time.time() - t2
|
| 326 |
+
base_val = parse(base_text)
|
| 327 |
+
tail = _clean(base_text)
|
| 328 |
+
tail = tail if args.verbose or len(tail) <= 240 else "..." + tail[-240:]
|
| 329 |
+
print(f"[2] backbone alone : {tail!r}")
|
| 330 |
+
print(f" value : {base_val if base_val is not None else 'no number parsed'}"
|
| 331 |
+
f" ({t_base:.1f} s, {len(tok.encode(base_text))} tokens"
|
| 332 |
+
f"{', answer forced' if forced else ''}; protocol: {src})", flush=True)
|
| 333 |
+
else:
|
| 334 |
+
print("[2] backbone alone : skipped (--no-baseline)")
|
| 335 |
+
|
| 336 |
+
# ---- 3. the physics model on the true initial condition
|
| 337 |
+
t3 = time.time()
|
| 338 |
+
params = mx.array([[item["a"], math.sin(item["phi"]), math.cos(item["phi"])]], dtype=mx.float32)
|
| 339 |
+
ic = build_ic_mlx(params) # the bridges' IC parameterization
|
| 340 |
+
u_field = np.array(fno(ic), dtype=np.float64)[0] # u(x, t=0.5) on the 128-grid
|
| 341 |
+
u_fno = fourier_interp(u_field, item["x0"])
|
| 342 |
+
t_fno = time.time() - t3
|
| 343 |
+
u_true = fourier_interp(solve(initial_condition(item["a"], item["phi"])), item["x0"])
|
| 344 |
+
if args.verbose:
|
| 345 |
+
ic_np = initial_condition(item["a"], item["phi"])
|
| 346 |
+
print(f" (IC parameterization check: max|build_ic - initial_condition| = "
|
| 347 |
+
f"{float(np.abs(np.array(ic)[0] - ic_np).max()):.2e})")
|
| 348 |
+
print(f"[3] physics model (FNO) : u({item['x0']}) = {u_fno:+.4f} ({t_fno*1e3:.0f} ms; "
|
| 349 |
+
f"the reference the coupled answer should match to +-{TOL})")
|
| 350 |
+
print(f" spectral solver : u({item['x0']}) = {u_true:+.4f} (ground truth)")
|
| 351 |
+
|
| 352 |
+
# ---- verdict
|
| 353 |
+
def mark(v):
|
| 354 |
+
return "n/a" if v is None else ("match" if abs(v - u_fno) <= TOL else "off")
|
| 355 |
+
if psi_val is None:
|
| 356 |
+
verdict = "PsiLM n/a (no number in the reply)"
|
| 357 |
+
else:
|
| 358 |
+
verdict = (f"PsiLM {mark(psi_val)} (|{psi_val}-{u_fno:.2f}| "
|
| 359 |
+
f"{'<=' if abs(psi_val-u_fno) <= TOL else '>'} {TOL})")
|
| 360 |
+
print("\n" + verdict + ("" if args.no_baseline else f"; backbone alone {mark(base_val)}"))
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
if __name__ == "__main__":
|
| 364 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PsiLM on Gemma 4 12B -- Apple Silicon (MLX). Verified 2026-09-06 on macOS / Python 3.11
|
| 2 |
+
# with: mlx 0.32.2, mlx-lm 0.31.3, transformers 5.16.1, torch 2.13.0, huggingface_hub 1.29.0.
|
| 3 |
+
#
|
| 4 |
+
# pip install -r requirements.txt
|
| 5 |
+
# python psilm_infer.py
|
| 6 |
+
#
|
| 7 |
+
mlx>=0.32
|
| 8 |
+
mlx-lm==0.31.3 # exact: the Gemma 4 loader (psilm.mlx.gemma_loader) goes through
|
| 9 |
+
# mlx_lm.utils internals; other versions are untested
|
| 10 |
+
transformers>=4.51 # chat template only (apply_chat_template); the model runs in MLX
|
| 11 |
+
torch>=2.4 # imported by psilm.stage2.qa (batching helpers); CPU wheel is fine
|
| 12 |
+
huggingface_hub>=0.30 # downloads the backbone from mlx-community
|
| 13 |
+
numpy>=1.26
|
| 14 |
+
safetensors>=0.4
|
| 15 |
+
|
| 16 |
+
# The PsiLM package itself (bridges, staged forward, Gemma loader, FNO, QA builder).
|
| 17 |
+
# Alternative to this line: clone the repository and set PSILM_REPO=/path/to/PsiLM.
|
| 18 |
+
psilm @ git+https://github.com/ryoji-info/PsiLM
|