DualRate Physics v1
A dual-rate 2D rigid-body physics predictor. A frozen, truncated Qwen3.5-0.8B
semantic encoder (first 12 of 24 layers + 8 readout tokens) reads the scene text
(header + recent frames) and produces a latent that is refreshed only every K=8
frames; between refreshes a fast 38.9M flow-matching expert (per-object tokens,
cross/self-attention, 10 Euler steps) advances the numeric object state
[px, py, vx, vy, angle, angular_velocity] every frame. The slow semantic path
is the "slow rate", the per-frame flow head is the "fast rate".
- ๐ฎ Interactive demo (ZeroGPU): https://huggingface.co/spaces/AlexWortega/physics-dualrate-playground
- ๐ป Code & full training/eval pipeline (GitHub): https://github.com/AlexWortega/dualrate-physics-llm
- ๐๏ธ Checkpoints:
ckpt_v1_60k.pt(final champion, step 60k) ยทckpt_v1_45k.pt(earlier) ยทckpt_v1_10k_ood.pt(10k-step checkpoint โ best mid-training OOD before LR-decay recovery; see the checkpoint-trajectory table inresults/RESULTS.md) - ๐ Semantic encoder:
Qwen/Qwen3.5-0.8B(downloaded from the Hub; its weights are not stored in these checkpoints โ the files hold only the expert, projection, and conditioning weights + config + normalizer).
Results โ ckpt_v1_60k.pt (champion)
Evaluated against the PhysicsLLMEngine LFM2-350M+LoRA text baseline.
| Metric | DualRate (ours) | Baseline | Factor |
|---|---|---|---|
| Single-step position error (teacher-forced val) | 0.052 px | LFM2: 22.64 px | ร435 |
| Single-step, eval rollout step 1 (200 scenes) | 0.255 px | โ | โ |
| PhysicsLLMEngine exact protocol, overall MSE | 0.650 | 862.21 | ร1327 |
| โ pong (baseline's worst case) | 0.131 | 5027.39 | ร38,377 |
| Wide OOD (6 unseen types ร 50 scenes, ctx10 / 5 steps) | 1.70 MSE | โ | โ |
| Inference | 0.067โ0.107 s/frame (A6000) | ~30โ60 s / 50 frames (CPU) | โ |
Exact protocol = bit-identical scenes (same generator code, seeds, difficulties as the
baseline's llm_mse_results.json), same prompt format, same per-step position MSE,
ctx = 10 frames, 5 autoregressive steps. Per-step MSE: ours [0.01, 0.09, 0.33, 0.89, 2.16]
vs baseline [80.32, 308.6, 732.58, 1274.23, 1915.31].
Dual-rate ablation (K = latent refresh period, exact protocol)
| K | overall MSE |
|---|---|
| 1 (refresh every frame) | 0.643 |
| 8 (refresh every 8 frames) | 0.650 |
The stale latent is nearly free: refreshing the Qwen latent 8ร less often costs ~1% accuracy. The slow semantic path can tick at โ rate; all per-frame precision lives in the fast flow head.
Honest caveats
- Long rollouts drift: short horizons are excellent (~7.8 px RMS at step 10), but beyond ~30 steps chaotic many-body dynamics + error compounding dominate (no rollout-aware training yet โ the obvious v2 lever).
- Trained on 54k val-derived scenes (17ร smaller than the baseline's 900k train split) yet wins.
- OOD generalization dipped mid-training (high LR) and recovered via cosine decay โ don't early-stop on seen-type metrics.
Repository contents
ckpt_v1_*.ptโ slimmed checkpoints (expert + projection + readout + config + normalizer; no optimizer state).results/โRESULTS.mdplus the raw evaluation JSONs (exact-protocol, wide-OOD, rollout, and the full checkpoint trajectory).code/โ thedualrate/model package and the training/eval scripts, so these weights are reproducible from this repo. Full project (data generators, baselines, tests): https://github.com/AlexWortega/dualrate-physics-llm
Loading
import torch
from transformers import AutoTokenizer
from huggingface_hub import hf_hub_download
# from your copy of src/dualrate/
from dualrate.config import DualRateConfig
from dualrate.model import DualRatePhysicsModel
from dualrate.normalization import StateNormalizer
ckpt = torch.load(hf_hub_download("AlexWortega/dualrate-physics", "ckpt_v1_60k.pt"),
map_location="cpu", weights_only=False)
cfg = DualRateConfig.from_dict(ckpt["config"])
norm = StateNormalizer(**{k: ckpt["normalizer"][k]
for k in ("state_offset", "state_scale", "delta_scale")})
tok = AutoTokenizer.from_pretrained(cfg.model_name)
model = DualRatePhysicsModel(cfg, readout_token_id=tok.pad_token_id)
missing, unexpected = model.load_state_dict(ckpt["model"], strict=False)
# `semantic.lm.*` keys are "missing" on purpose โ they come from the Qwen3.5 download.
model.eval().cuda() # requires a CUDA GPU: Qwen3.5 linear-attention uses Triton kernels
โ ๏ธ A CUDA GPU is required for inference: the Qwen3.5 linear-attention layers use flash-linear-attention Triton kernels that have no CPU backend.