Skip to content

Add UMA-S-1.2 support (charge-balanced channels) + edge-frame & energy-head fixes - #1

Open
janweinreich wants to merge 132 commits into
moritztng:masterfrom
janweinreich:uma-s-1.2-support
Open

Add UMA-S-1.2 support (charge-balanced channels) + edge-frame & energy-head fixes#1
janweinreich wants to merge 132 commits into
moritztng:masterfrom
janweinreich:uma-s-1.2-support

Conversation

@janweinreich

@janweinreich janweinreich commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Adds uma-s-1.2 support to TT-Atom (run via UMA(atoms, checkpoint="uma-s-1p2.pt")), plus fixes uncovered while validating it — rebased onto current master (after its recent perf/kernel overhaul). Full 757-system A/B vs fairchem: no genuine force errors, and 691/757 pass the strict bar (above the 672 the pre-perf-pass backbone reached). Draft, for review.

Six commits on current master:

Feature

  1. uma-s-1.2 support: charge_balanced_channels + fp32 energy head — uma-s-1.2 needs fairchem's charge_balanced_channels (missing → force PCC ~0.83): a self-adjoint per-system l=0 mean-subtraction of channels [cs:ce], re-balanced to the system charge after each block. Threaded through the forward (Backbone.node_embedding/GraphContext.node_meanM), the reverse VJP (backbone_bw), the disjoint batch, and the traced loop (single + batched); export_weights writes charge_channel_start/end. calculate/from_uma already source charge/spin, so this threads the numeric charge into the force calls. A self-review also hardened the batched/multi-card paths: MultiCard builds the mean operator (no hang on uma-s-1.2), the traced batch uses the true per-system natoms, charged batches of unequal atom counts are rejected with a clear error, and the vendored Wigner-D asset now ships in the wheel (pyproject package-data). Also emits the energy head + node-sum in fp32 (the bf16-rounded reference-subtracted energy was the dominant device energy error on high-|raw| systems).

Correctness fixes

  1. smooth quaternion edge frame + generic eval rotation — replaces the singular ZYZ-Euler edge→+Y frame (±Y-axis singularity → ~300 meV/Å forces at exactly-symmetric geometries) with fairchem's default two-chart quaternion frame (vendored, MIT). Symmetric-molecule force MAE ~300 → <8 meV/Å. (The trace-width crash the Euler zeros caused is independently handled on master now via rotation.gather_coef at the cached sparsity pattern, so only the frame is ported.)
  2. fp32 radial-MLP backwardfixes a force regression in master's on-device-backward work. The radial-MLP LayerNorm VJP (RadialMLP._ln_bw) runs in bf16 by default, unlike the SH-norm/gate backwards which reuse fp32-accurate stats. On an OOD geometry the bf16 radial VJP mis-directs the force: el_Sn_cmp (2-atom compressed β-Sn) dropped a whole component → traj PCC 0.73 / 230 meV/Å, energy still correct. Diagnosed vs an fp64 mirror.radial_mlp oracle (device rad.bw PCC 0.35; same math in fp64 from the same bf16 cache → PCC 1.0, so it's the bf16 arithmetic). Fix: run the small radial backward in fp32 (fp32 weight copies incl. the transformed w6; forward stays bf16; the opt-in bf16-only fused_ln_bw kernel path is unchanged). Result: el_Sn_cmp1.0000, ~+4% eager. Adds the missing test_forces.py assertion that the device radial finish matches the oracle.

Docs / tests

  1. chore — track the vendored tt_atom/assets/wigner_d_coefficients.pt.
  2. test — gated uma-s-1.2 real-weight parity (auto-skips without the checkpoint) + gen_golden_real.py --ckpt-path.
  3. docs — README note + docs/uma-s-1p2-validation.md.

Validation — full 757-system A/B vs fairchem on CPU (uma-s-1p2.pt), on current master

757 systems, 2–99 atoms, ~63 elements, molecular + 491 periodic (omol/omat/oc20/odac/omc; 175 oc20 surfaces, 40 nanoparticles, S22, radicals, charged TM complexes, EOS curves).

  • No genuine force errors. 741/757 keep pooled force PCC > 0.999 (754/757 > 0.99); the 3 below 0.99 are the zero-force perfect-lattice artifact (|F|≈0 by symmetry, traj Fmae < 1.6 meV/Å). Showcase systems — Pt(111) CO-oxidation, Cu₅₅+CO, [Cu(EDTA)]²⁻ bare + solvated — are all PCC 1.0000. The one regression master's bf16 radial backward introduced (el_Sn_cmp) is fixed by commit 3.
  • 691 / 757 pass the strict bar (force PCC > 0.99, energy < 5 meV/atom, stress PCC > 0.99), above the 672 the earlier backbone reachedmaster's numerics plus the fp32 head net lower the bf16 energy floor. The misses are that floor on the highest-|raw| systems (heavy-metal diatomics/crystals, up to ~11 meV/atom), forces PCC ~1.0, all under chemical accuracy (~43).
  • Stress (periodic): median PCC 0.99996, 488/491 ≥ 0.99.

Performance — MgO supercells, ms/call, TT (Wormhole, ttnn 0.68.0) vs 16-thread CPU

atoms TT eager TT trace CPU (16t) CPU / TT-best
8 103 75 138 1.8×
64 317 319 860 2.7×
216 1059 973 5326 5.5×
512 2794 OOM

Up to 5.5× at 216 atoms (trace); ~2.2× faster than the pre-perf-pass backbone. master's kernel work restores the trace path at scale (captures through 216-atom periodic / 192-atom molecular; molecular ethanol 90→47 ms trace, ~1.9×) and reaches the 512-atom cell in eager — only the 512 trace still exceeds single-card DRAM. The fp32 radial-backward fix adds ~4%. Full writeup in docs/uma-s-1p2-validation.md.

Testing

  • Host tests/test_symmetry.py (no card). Card random-weight suite green; test_forces.py now also asserts the device radial finish vs the fp64 oracle. Gated tests/test_realweight_uma_s_1p2.py: 3 passed with a generated golden; auto-skips otherwise.

Notes

  • ttnn 0.68.0. uma-s-1.2 via --checkpoint / UMA(checkpoint=...); bundle_cache auto-build stays uma-s-1. tt_atom/quaternion.py vendored verbatim from fairchem (MIT). UMA weights gated, not included.

🤖 Generated with Claude Code

moritztng and others added 30 commits June 29, 2026 13:51
…it, dirs

- Apache-2.0 LICENSE + NOTICE clarifying weights are not included and separately licensed.
- pyproject: package tt_atom; deps numpy<2/torch/ase; fairchem-core as optional 'reference'
  extra (separate env, numpy>=2 conflict documented).
- .gitignore: exclude weights/checkpoints; allow small committed golden fixtures in tests/data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tests/gen_golden.py builds the fairchem eSCNMDBackbone (eSEN/eSCN-MD/UMA backbone) with
deterministic random weights, runs forward + autograd forces on a molecule (ethanol) and a
periodic system (rattled Si), and dumps a single npz with inputs, host-precomputed geometric
terms (wigner, edge_envelope, x_edge, init node feats), full weights, per-module activations,
and outputs (node_embedding, energy, forces). Runs in the separate reference env (numpy>=2);
the ttnn port (numpy<2) compares against the npz on disk, decoupling the two environments.

Committed tiny-config goldens (sphere_channels=32, lmax=mmax=2, 2 layers):
  tests/data/golden_tiny.npz       ethanol, 9 atoms / 72 edges
  tests/data/golden_bulk_tiny.npz  rattled Si diamond, 4 atoms / 112 edges

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ase 3 start)

Single place for the numerics policy (HiFi4 + fp32_dest_acc + packer_l1_acc, validated at
matmul PCC 0.999996 on p150) and device/mesh open with program cache. ttnn imported lazily.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1.0 on p150)

The compute heart. Flattened [E, (lmax+1)^2 * C] layout puts each order m in a contiguous
tile-aligned column block, so the SO(2) conv is column slices + per-m GEMMs + elementwise --
no tile-dim-1 slicing. Includes the on-device RadialMLP (Linear/LayerNorm/SiLU) that produces
per-m radial weights. Config-driven (any lmax/mmax/channels).

tests/test_so2.py validates so2_conv_1 (with radial + extra-m0 gating) and so2_conv_2 against
the fairchem golden on real hardware: out PCC 0.99999/1.0, gating PCC 0.99999. tests/util.py +
conftest.py provide the golden loader and a session-scoped device fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors EquivariantRMSNormArraySphericalHarmonicsV2: l=0 centering, degree-balanced component
RMS, per-degree affine, l=0 bias -- pure reductions/elementwise. tests/test_norm.py: PCC 0.990
vs golden block0.norm_1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mwise + assembly)

gen_golden.py now also dumps host@to_grid_mat/from_grid_mat (fixed SO3_Grid transforms for
GridAtomwise) and host@sys_node_embedding. Regenerated tiny goldens; existing SO2/norm parity
tests still pass (3 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the remaining device modules and assemble the backbone:
- grid.py    GridAtomwise (transpose-matmul to/from S2 grid + 3-layer MLP); PCC 0.99999
- activation.py GateActivation (m-primed sigmoid gate via slice+concat gather)
- edgewise.py Edgewise message block (embedding gather, bmm rotate, SO2x2 + gate,
              envelope, one-hot-matmul scatter-add); PCC 0.99999
- model.py   GraphContext (host geometric terms -> device) + Backbone + energy head

End-to-end vs fairchem golden (tiny cfg, random weights): node_embedding PCC 0.99998,
energy -1.3906 vs -1.3826 (0.6% rel). All 7 parity tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…9998

Analytic forces (F=-dE/dpos), production path, NOT finite-diff. The heavy dE/dfeature
terms are the transposes of the forward GEMMs and run on device; the radial-MLP backward
(its LayerNorms) is finished on host autograd, cutting the device tape at the radial output.

- tests/mirror.py: bit-exact PyTorch transcription of the device forward (PCC 1.0 / energy
  bit-identical vs golden) -> the autograd oracle for the VJP and the future host force.
- forces.py: hand-written VJPs (rmsnorm/gate/so2/grid/bmm/scatter/energy-head) producing
  device adjoints g_x_init, g_wigner, g_wigner_inv, g_envelope + per-conv g_rad.
- forward modules cache tape intermediates; GraphContext gains source one-hot for the
  gather-backward matmul.

Validated vs mirror autograd (tiny cfg, random w): g_x_init 0.99997, g_wigner 0.99995,
g_wigner_inv 0.99991, g_envelope 0.99988, g_x_edge 0.99997. 8 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…0.99998

Close the analytic-force path (phase 4). NOT finite-difference.

- geometry.py: differentiable host pos->{wigner, x_edge, envelope, edge-degree x_init} map
  (vendored pure-torch e3nn/fairchem rotation math, MIT). gamma roll fixed (gauge-invariant).
  Forward terms match golden exactly (dist/x_edge/envelope PCC 1.0; energy gauge-invariant
  bit-exact).
- forces.energy_and_forces: device reverse VJP (dE/d geometric inputs, heavy transpose-matmuls
  on device) + torch.autograd through host geometry (cheap d(geometric)/dpos) -> F=-dE/dpos.
- gen_golden: also dumps to_m, gaussian offset/coeff, edge_distance(_vec).

End-to-end vs fairchem golden (tiny, random w): energy 0.6% rel; force PCC 0.99998,
cosine 0.99998, MAE 3.3e-4 (|F|max 0.21). 10 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- weights.py: WeightBundle loader (.npz: state_dict + fixed geometry buffers) with
  verify_coverage() for real-checkpoint drop-in checking. ttnn-env loadable.
- tools/export_weights.py: fairchem checkpoint (or random) -> bundle, run in fairchem env;
  bridges the numpy<2 (ttnn) / numpy>=2 (fairchem) split. Roundtrip verified.
- geometry.py: csd_embedding (system charge/spin/dataset, PCC 1.0 vs golden) + radius_graph.
- calculator.py: TTAtomCalculator (ASE) — radius graph + sys embedding + device energy and
  analytic forces.
- examples/relax.py + model_tiny_demo.npz (732K, random weights): FIRE relaxation of ethanol
  CONVERGES on TT (E -1.36 -> -1.58 eV, 39 steps, fmax 0.042 < 0.05).
- lazy package API; editable install. 12 tests pass (incl calculator relaxation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real, reproducible measurement harness (warm device-resident eval, Medges/s, end-to-end vs
PyTorch-CPU reference via the bit-exact mirror). Honest pre-optimization baseline: the forward
is currently memory-movement/op-count bound, not FLOP-bound — matmuls run at <1 TFLOP/s and the
per-edge Wigner bmm + so2 data-movement dominate. Optimization (matmul configs, op reduction,
layout, trace, batching, multi-card) is the next focused effort; no perf claims until measured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uracy-safe

bf8 weights: forward+forces PCC 0.99997 (no accuracy loss). bf8 Wigner operands on the
per-edge rotation bmm (the single costliest op): ~3x faster in isolation, ~14% end-to-end,
PCC 0.99997 — enabled via GraphContext(fast=True) for the energy-throughput path. The analytic
force backward stays bf16 (bf8 would mix dtypes in the transpose-matmul adjoints). Threaded
fast through TTAtomCalculator/Backbone. 12 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-m so2_m_conv reshaped its [E,2K] real|imag block to [E,2,K] before the
matmul, which made the length-2 part-dim a tile dimension (padded 2->32: a 16x
data blowup + a per-edge batched matmul). Replacing it with two plain 2D GEMMs on
the sliced halves is the identical math and runs ~80x faster on that path.

Measured (p150, N=250 E=4834, full cfg C=128): so2_1 55.8->4.5ms, so2_2 30->2.5ms,
block 125->46ms (2.7x); N=54 dev 44.9->18ms. so2_bw updated to the matching flat
VJP. All 12 tests pass; forces PCC preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-edge Wigner bmm [E,9,9]x[E,9,C] was launch-bound (~2.9us/edge: tiny 9x9
tile-padded matmuls, one batched launch per edge). W has a fixed sparsity pattern
(block-diagonal in l, permuted by to_m), so the rotation is now elementwise
column-block MACs in flat [E,9*C] layout -- one launch over all edges per nonzero.
GraphContext packs W to its structural nonzeros; the whole edgewise/SO2 chain now
runs flat [E,9*C] (no [E,9,C] tile padding). Forces VJP updated: the rotation
backward emits per-edge coefficient adjoints, scattered to dense dW for the host
dW/dpos autograd (pattern-only is exact for the force -- off-pattern dW/dpos==0).

Measured (p150, N=250 E=4834, full cfg): block 46->24ms; full forward 251->48ms
(5.2x vs the pre-optimization baseline). Trace now gives 2.3x at small E
(dispatch-bound). 12 tests pass; end-to-end force PCC still >=0.98.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…apes

SO2Convolution now returns flat [E,nsph*H] and GateActivation operates flat
[E,nsph*H], so the edgewise message chain (gather->rotate->so2->gate->so2->
envelope->rotate->scatter) is flat [E,nsph*C] end to end with no 9->32 tile
padding. gate_bw/so2_bw updated to the flat VJP.

Measured (p150, N=250 E=4834): full forward 48->34ms; gate 3.8->~1ms/block.
Trace replay 2.9x at small E (9ms, dispatch-bound), 1.5x at medium. 12 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MultiCard runs one worker process per Tenstorrent card (pinned with
TT_VISIBLE_DEVICES, weights resident), streaming independent systems through a
shared queue. Each worker pins torch to one host thread: the host geometry
otherwise grabs every core, so N workers oversubscribe the CPU and throttle each
other (4-card was *slower* than 1 until this). bench_multicard.py measures 1- vs
4-card aggregate throughput.

Measured (p150 x4, 128-atom Si, program-cache-warm): 0.022 -> 0.088 Medges/s,
3.95x scaling (near-linear). End-to-end throughput is host-geometry-bound.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bench_throughput.py: --fast flag + honest energy-rel-err column + device-vs-CPU
  speedup; bench_multicard.py + plot.py generate the release charts from real JSON.
- assets/: device_vs_cpu.png (speedup grows to 5.3x at 250 atoms), multicard_
  scaling.png (3.95x on 4 cards) -- generated from measured data, not hardcoded.
- README.md: honest accuracy status (random-weight validated, real weights drop-in,
  none shipped), measured perf table, design, quickstart, reproduce steps, license.
- RELEASE.md: draft announcement + social post with the real numbers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the golden harness to the released facebook/UMA uma-s-1 checkpoint:
load EMA weights, host MoLE merge via fairchem's own merge_mole=True path -> a
plain eSCNMDBackbone, capture per-module activations (spectral atomwise), host
geometric terms, and the per-task energy normalizer (rmsd scale + element refs).

Merge correctness ANCHOR (ethanol, omol), measured on this box:
  merged plain backbone vs unmerged-MoE oracle: E rel err 1.3e-12, F PCC 1.0,
  F MAE 2.1e-7 eV/A  (target PCC>0.999).
  recompute via energy head + E=rmsd*E_raw+sum refs[Z]: E rel 1.1e-7 vs oracle.
Goldens dumped to ~/.ttatom_run/goldens_real (NOT committed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tic-force VJP

uma-s-1 uses the spectral FF (SpectralAtomwise: two per-degree SO3_Linear GEMMs
with a gate between, gate driven by an l=0 scalar MLP) where TT-Atom only had the
grid FF. New tt_atom/spectral.py keeps features in the SH basis (per-l shared GEMM,
bias on l=0, non-m-primed gate). _Block selects grid vs spectral by cfg ff_type;
grid.py path untouched. forces.py gains spectral_bw (reverse VJP) selected in block_bw.

Validated on the p150 vs the real uma-s-1 golden (ethanol, block 0):
  forward: gating/so3_linear_1/atomwise-out all PCC 0.99999 (target >=0.98)
  backward: spectral_bw vs torch autograd PCC 0.99999, rel-L2 0.0044
All 12 existing (random-weight, grid) tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…coverage

Host-side gaps for real uma-s-1 weights (all cheap, pos->device inputs unchanged):
- geometry.csd_embedding now dispatches pos_emb vs rand_emb (uma-s-1 uses rand_emb:
  charge lookup at idx charge+100, spin at idx spin) and takes the per-task dataset
  token (omol/omat/...). Validated vs golden sys_node_embedding: PCC 1.0, rl2 0.0.
- calculator applies the energy normalizer E=rmsd*E_raw+mean+sum refs[Z], F=rmsd*F_raw
  (rmsd/mean/elem_refs from the bundle; identity for legacy random bundles).
- weights.py: WeightBundle exposes task/scale_rmsd/scale_mean/elem_refs; expected_keys
  branches on ff_type (spectral so3_linear/scalar_mlp) and chg_spin_emb_type (rand_emb).
  verify_coverage on the real merged bundle: ok=True, 145 keys, none missing.
All 12 existing tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bundle)

5 tests gated on a real-weight golden bundle (TTATOM_REAL_GOLDEN, default
~/.ttatom_run/goldens_real/ethanol_omol.npz); auto-skip when absent so the suite
stays green without UMA access. Covers: config is real uma-s-1, MoLE merge anchor
(PCC>0.999 vs oracle), verify_coverage, spectral atomwise per-module (PCC>=0.98),
and end-to-end device E (rel<1e-2) + forces (PCC>0.99) vs the fairchem oracle.
Measured on p150: all pass. Full suite 17 passed (12 + 5); 5 skipped without bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README/RELEASE: replace the 'real-weight accuracy pending checkpoint access' caveat
with measured uma-s-1 numbers (single p150, ethanol/omol, vs fairchem oracle):
merge anchor E rel 1.3e-12 / F PCC 1.0; end-to-end E rel 1.8e-7, F PCC 0.99958,
MAE 3.4e-3; FIRE relaxation converges (fmax->0.049 in 58 steps). States clearly:
validated on uma-s-1 via fairchem reference, single-p150; perf/multi-card unchanged.

tools/export_weights.py gains --uma-s-1: host MoLE-merge (fairchem merge_mole path)
to a plain backbone for a fixed composition, writes a clean WeightBundle with the
real energy head + per-task normalizer (scale@*), no activations. Verified: bundle
verify_coverage ok, calculator E=-4218.474 eV (rel 4.7e-7 vs oracle), |F|max 0.559.
No weights committed/redistributed; checkpoint stays gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
radius_graph now takes (cell, pbc) and returns (edge_index, cell_shift): the
cartesian per-edge image offset, matching fairchem radius_graph_pbc +
get_pbc_distances exactly. HostGeometry/forces/calculator thread the shift into
edge_vec (constant wrt pos, so analytic forces are unaffected). Materials tasks
now work.

Validated on bulk Si diamond (omat, real uma-s-1, p150):
  graph reproduces fairchem edge_index + edge_distance_vec bit-for-bit (172 edges);
  E rel err 2.99e-4, force PCC 0.99999, MAE 6.5e-3 eV/A vs fairchem oracle.

tests/test_periodic.py (auto-skips without the gated golden). 20 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- TTAtomCalculator gains task_name (mirrors FAIRChemCalculator); validated
  against the bundle's baked-in task. MoLE routing consumes the dataset token,
  so the task is fixed at merge/export time — task_name mismatch raises rather
  than silently using the wrong normalizer. Exposes .task_name.
- gen_golden_real.py: 'slab' system (Cu(100)+H, oc20, mixed pbc=[T,T,F]) +
  stores in@pbc.
- tests/test_periodic.py parametrized over periodic tasks (omat bulk + oc20 slab).

Task heads validated end-to-end vs fairchem (real uma-s-1, p150), all graph regimes:
  omol ethanol   E rel 1.8e-7  F PCC 0.99958
  omat bulk Si   E rel 3.0e-4  F PCC 0.99999
  oc20 CuH slab  E rel 8.6e-5  F PCC 1.00000
odac/omc use the identical data-driven path (dataset token + per-task normalizer
from the checkpoint); enable by exporting a bundle with --task. 21 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
uma-m-1p1 export succeeds (merged-only, 315 tensors, fits host RAM) but its
architecture is lmax=4/mmax=2 — it uses spherical-harmonic coefficient
subselection (25->19 coeffs) that the port does not implement (uma-s-1 has
lmax==mmax==2). Backbone now raises a clear NotImplementedError instead of a
cryptic einsum shape error, naming uma-s-1 as the supported default.

uma-m ceiling (honest): (1) needs the mmax<lmax subselection path (not
implemented); (2) the oracle+merge parity harness OOMs on this 30GB host (the
merged-only export fits, the 2x-load golden gen does not). uma-s-1 stays default.

gen_golden_real.py --ckpt lets the golden harness target any UMA checkpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Profiling (real uma-s-1, ethanol, p150) shows the device forward+backward is
~96% of a calculate() (host geometry only ~3 ms) and is host-DISPATCH-bound for
these graph sizes — the ttnn-trace lever, not host amortization. New
tt_atom/trace.py TracedEngine captures the fwd+bwd device op-stream once for a
fixed topology and replays it, refreshing only the pos-dependent inputs (Wigner
coefs, radial edge emb, envelope, node init) in place each step.

TTAtomCalculator(trace=True): captures on first call, re-captures automatically
when the neighbour list changes (atom crosses cutoff) so results stay correct.

Fixes required for capture:
- energy_bw's dE/dE seed (ttnn.ones) + zero-pad now cached on the backbone once;
  ttnn.ones does a device fill that trace capture forbids (hangs). (zeros memset
  is fine — the forward's rotation zeros already captured cleanly.)
- must execute_trace after capture too: begin/end_trace_capture RECORDS without
  executing, so the capture-step outputs are uninitialized until the first replay.

Measured (p150): forward-only replay 2.6x; full fwd+bwd e2e per step 86->40 ms
(2.14x); real FIRE relaxation identical 46-step trajectory, same final energy
(diff 0.0), 119.5->51.4 ms/step = 2.33x wall clock. Forces bit-for-bit eager.

tests/test_trace.py (uses committed demo bundle) + benchmarks/bench_trace.py.
conftest reserves a trace region on the shared device. 22 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ examples

- tt_atom/cli.py + [project.scripts] tt-atom: info / verify / relax / md /
  convert-checkpoint. relax & md take --trace; relax/md accept --input (ASE
  file) or --molecule.
- Converter now embeds the fairchem reference E/F+input (ref@*) in the bundle so
   closes the roundtrip ON DEVICE without a fairchem env (the two
  numpy worlds can't share a process). Verified uma-s-1 ethanol: device E rel
  4.75e-7, F PCC 0.99945 -> PASS.
- convert-checkpoint runs the export in the reference env; if fairchem is absent
  it prints the exact reference-env command instead of failing obscurely.
- pyproject: torch pin, explicit note that ttnn is not a PyPI dep (obtain from
  tt-metal wheel), console entry.
- examples/: md.py (Langevin, --trace), periodic.py (bulk Si PBC), batch.py
  (multi-card fan-out). All smoke-tested on the demo bundle.

22 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lead with the one-line fairchem->TT-Atom swap (TTAtomCalculator mirrors
FAIRChemCalculator's task_name surface). Real uma-s-1 accuracy table across 3
validated tasks (omol/omat/oc20, all graph regimes, all meeting E rel<1e-3 &
F PCC>0.99); trace MD perf table (2.14x per step, 2.33x FIRE); ttnn-not-on-PyPI
install note; uma-m-1p1 ceiling; CLI + examples documented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pyflakes-clean across tt_atom/ (unused torch imports in activation/grid/
spectral/cli, unused compute_kernel_config + backbone_bw ttnn in forces, unused
VJP locals Cin/nc/parts/N/x; dead imports in tests/benchmarks). No behaviour
change; 22 tests pass. Random-weight scaffolding kept — it is the always-
available (ungated) CI + demo path (grid FF, module parity tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
moritztng and others added 26 commits July 8, 2026 19:17
… release gate, install docs

- Single-source tt_atom.__version__ from package metadata (was hardcoded)
- CI: host-only build/import/metadata checks (device/accuracy/OOM/perf gate stays on hardware)
- release.yml: tag v* -> build, tag/version guard, GitHub Release + wheel; dormant PyPI
  Trusted-Publishing job (un-gate via PYPI_ENABLED)
- CHANGELOG.md + RELEASING.md (explicit release gate: accuracy parity, no OOM, no perf regression)
- README install: pin a tested release tag, not master; nightly/source fallback
…backs)

tt-atom is now the custom-kernel-only, highest-performance build for uma-s.
The per-edge Wigner rotation always runs the custom ttnn.experimental.fused_rotate
kernel; the slow addcmul-MAC fallback is gone and unsupported shapes raise a clear
error instead of silently degrading.

rotation.py:
- Remove all env gating and A/B/ablate knobs (TT_ATOM_FUSED_ROTATE, _FUSED,
  _FUSED_BW, TT_ATOM_FUSED_GC, _FUSED_GC, TT_ATOM_GC_MIN_EDGES, TT_ATOM_FUSED_NOCACHE,
  TT_ATOM_ABLATE_GC). fused_rotate is unconditional for the shapes it supports.
- Delete the addcmul-MAC fallback in rotate/rotate_bw. A shape the kernel cannot run
  (uma-m: rectangular 19x25, W=256 overflows L1) now raises a clear RuntimeError.
- Coefficient adjoint (rotate_bw gc): keep BOTH the fused gc kernel (large graphs) and
  the ones_bd segment-sum GEMM (small/mid-N), edge-gated by a fixed constant
  (_GC_MIN_EDGES, no longer an env var). The gc kernel's product CB clashes with the
  trace's L1-resident tensors at small/mid-N, so the GEMM is the correct path there.
  This keeps uma-s correct with no L1 clash / OOM at any size.
- Fix a latent id-recycling bug in _group's cache (surfaced now that the kernel path is
  always taken): add an identity guard so a recycled id(ij) can't return a stale grouping.

trace.py: drop the expanded-coef cache right before trace capture so the on-device coef
expansion is recorded INTO the trace and refreshed on every replay from the in-place
compact coef. Without this, the warmup-built expansion was frozen at the capture step and
traced forces went stale after a refresh (bug fixed: traced==eager to PCC 1.0 / maxdF 0).

pyproject.toml: drop the ttnn wheel dep (ttnn now comes from the source tt-metal build
with the custom op). Keep numpy<2, torch, ase and all versioning/CI/release machinery.

custom_kernels/: sync the op backup with the authoritative source (fused_rotate + gc +
gate + ln_bw, one library, 4 kernels) and document the exact build recipe (tt-metal
b5522097 + 3 registration edits). README Install rewritten for the from-source path.

tests/test_umam.py: uma-m is unsupported in this build -> assert it raises the clear error.

Verified on a Blackhole p150a (real uma-s-1): pytest 51 passed; energy rel-err 1.8e-7..5.4e-4
and force PCC 0.99959..0.99999 across molecular/periodic/slab goldens; fused_rotate 4.3x the
split+addcmul MAC in isolation (PCC 0.999995); end-to-end trace MD 1.4-1.7x faster than the
MAC path with no regression at any size; OOM ceiling unchanged (works to N=2662, E~93.7k).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…backs; source-ttnn required)

Verified on Blackhole p150a: pytest 51 passed/1 skipped; fused_rotate loads; rotation 4.3x
isolated, 1.4-1.68x e2e traced MD (N=54-2662), no OOM/accuracy/perf regression. uma-m raises
(unsupported in this build). ttnn no longer a pip dep (source build). Versioning intact.
…-kernel build needs source ttnn; a pip wheel can't run standalone)
…ls branch

The fused_rotate op is now pre-integrated on tt-metal's moritztng/tt-atom-kernels
branch, so install no longer copies the op source or applies the 3 registration
edits by hand — just clone that branch, build, install tt-atom, run.

custom_kernels/README stays as the authoritative op-source backup, reframed as
the recipe for re-integrating the op onto a newer tt-metal commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rnels)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified on hardware (moritztng/tt-atom @ 8d759240): build_metal.sh's
install target already auto-stages _ttnn.so into ttnn/ttnn/, and its
baked RPATH finds _ttnncpp.so in build_Release/lib without any copy —
the old `cp build/lib/_ttnn.so ttnn/ttnn/_ttnn.so` step was dead weight.
`pip install -e .` (tt-metal's own dev-install path, ~24s, no rebuild)
replaces it and also drops the need for manual PYTHONPATH exports.
Confirmed via `import ttnn`, op registration, and TT-Atom's on-device
end2end/forces/edgewise pytest suite (all pass) under the new flow.
Investigated uma-l support (README "Compared to fairchem" listed it as a gap
to close). Meta has never released a uma-l checkpoint -- facebook/UMA on HF
ships only uma-s-1(.1/.2) and uma-m-1p1, and the UMA paper abstract describes
scaling small/medium via MoLE rather than a third dense tier. There is
nothing to load or validate.

Along the way, found the README's "What it supports"/"Accuracy"/"Compared to
fairchem" sections still claimed uma-m-1p1 passes parity -- true when written,
but the later custom-kernel-only refactor (59619cf) dropped the MAC fallback
uma-m needed, and now tt_atom/rotation.py raises for its rectangular rotation
(tests/test_umam.py already anchors this). Fixed those three sections to match
reality and added a "Model coverage" section explaining both facts: uma-l
doesn't exist, and uma-m -- the closer, real precedent -- is unsupported by
this kernel for a documented L1-budget reason a larger checkpoint would only
make worse.

No code changes: bundle_cache.build_bundle already raises a clear ValueError
for any model string other than "uma-s-1", so an unknown checkpoint name was
never a silent failure mode.
The pypi-publish job was removed in f8028e8 (tt-atom needs a source
tt-metal build, so a pip wheel can't run standalone), but the workflow's
top-of-file comment still said it "(once enabled) publishes to PyPI via
Trusted Publishing" — contradicting the NOTE at the bottom of the same
file and RELEASING.md. Update the header to match actual behavior.
- test_multicard.py: parity test - MultiCard sharding 8/10 independent systems
  across up to 4 cards must give bit-exact energies vs running the same
  systems sequentially on 1 card. Sharding is bit-exact by construction (each
  system is evaluated once, on one worker, no cross-system batching unlike
  disjoint-union batching), so this exercises the real queue/dispatch/gather
  path. PASSED on qb2.
- bench_multicard.py: sweep 1..N cards (was only 1 vs 4) for a fuller scaling
  curve.
- README.md: note the TT_MESH_GRAPH_DESC_PATH workaround needed on qb2, where
  UMD misreads a single-chip P150 board as a dual-chip P300 and blocks opening
  any device (single-card included) until the mesh graph descriptor path is
  set explicitly - also needed in the parent process since MultiCard workers
  inherit env from it.

Measured on qb2 (4x P150, examples/model_tiny_demo.npz, 64 systems x 128
atoms): 1/2/3/4 cards -> 0.196/0.338/0.457/0.579 Medges/s, i.e. 1.72x/2.33x/
2.95x scaling vs 1 card. Sub-linear (vs the original 3.95x@4 measured
elsewhere with a larger model): fixed per-worker overhead (JIT
compile/dispatch) is a larger fraction of this tiny demo models per-system
compute, so it amortizes less across cards - matches the fixed-overhead
pattern already seen in tt-bio predict/esmc multi-card fanout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds fairchem charge_balanced_channels, required by uma-s-1.2 (missing -> force PCC ~0.83). balance_l0 is a self-adjoint per-system l=0 mean-subtraction on channels [cs:ce] re-balanced to the system charge after each block; threaded through the forward (Backbone.node_embedding/GraphContext.node_meanM), the reverse VJP (backbone_bw), the disjoint batch (BatchedGraph.charge), and the traced loop (single + batched). calculator/from_uma already source charge/spin (recent upstream fix); this threads the numeric charge into the force calls for the balance target. export_weights writes charge_channel_start/end (cs==ce => disabled = uma-s-1).

Also emits the energy-head output + node-sum (and the batch segment-matmul) in fp32: the per-node reference-subtracted energy was re-quantized to bf16 (~2^-8*|raw|/N), the dominant device energy error for high-|raw| systems (heavy metals, radicals, MgO). Forces are unaffected (their VJP seeds from the head weights, not the value). Rebased onto the current on-device-backward master; balance/head hook into the unchanged Backbone loop + GraphContext.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vendored fairchem Wigner-D coefficient table is a needed source asset caught by the blanket *.pt ignore; add a !tt_atom/assets/*.pt negation mirroring !tests/data/*.npz.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the singular ZYZ-Euler edge->+Y frame (geometry._wigner), whose +-Y-axis coordinate singularity gave wrong analytic forces at exactly-symmetric geometries (~300 meV/A, ethylene/acetone/pyridine). Ports fairchem_s DEFAULT two-chart quaternion frame + l<=4 quaternion->Wigner kernels (vendored MIT, byte-identical to fairchem) into tt_atom/quaternion.py; geometry._wigner dispatches to it (Euler kept behind use_quaternion=False for A/B). The trace-width crash the Euler zeros caused is independently handled upstream now (rotation.gather_coef at the cached sparsity pattern), so only the frame is ported here.

A residual bf16 gauge-leak remained for molecules whose symmetry axis lands on a coordinate plane; the model is rotationally equivariant, so the calculator evaluates molecules in a fixed generic orientation and rotates the force back (deterministic; molecules only, periodic/stress untouched). Result: symmetric-molecule force MAE ~300 -> <8 meV/A. Host-only regression in tests/test_symmetry.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d cells)

The on-device radial-MLP LayerNorm backward (RadialMLP._ln_bw: mean, x-mu, rsqrt) runs in bf16 by default, unlike the SH-norm (rmsnorm_bw) and gate (gate_bw) backwards which reuse fp32-accurate stats. On an out-of-distribution geometry the bf16 radial VJP mis-directs the force: el_Sn_cmp (2-atom compressed beta-Sn) drops a whole force component -> traj force PCC 0.729, MAE 230 meV/A, while the energy stays correct (forward fine, only the gradient wrong). Diagnosed vs an fp64 mirror.radial_mlp oracle: device rad.bw PCC 0.35-0.60; the same math in fp64 from the *same bf16 cache* recovers PCC 1.0 -> it is the bf16 arithmetic, not the cache.

Run the default (non-fused) radial backward in fp32 (fp32 weight copies incl. the transformed w6; upcast cache/g_out; return fp32 so the accuracy survives to the force). Forward stays bf16; the opt-in bf16-only fused_ln_bw kernel path is unchanged (documented tradeoff). Result: device rad.bw matches the oracle (PCC 1.0000); el_Sn_cmp 0.729 -> 0.9997 (9.5 meV/A); ~+4% eager. Adds the missing assertion to tests/test_forces.py that acc["x_edge"] (device radial finish) matches the oracle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tests/test_realweight_uma_s_1p2.py mirrors test_realweight.py for uma-s-1.2: device energy + analytic forces vs the fairchem oracle, asserting charge balancing is active (cs<ce). Auto-skips when the gated golden is absent. gen_golden_real.py writes charge_channel_start/end into the golden config and takes --ckpt-path to build from a local checkpoint file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents running uma-s-1.2 via UMA(atoms, checkpoint=uma-s-1p2.pt) + docs/uma-s-1p2-validation.md: full 757-system A/B on current master with these fixes -- 691/757 pass (above the 672 the pre-perf-pass backbone reached), no genuine force errors (741/757 force PCC >0.999; el_Sn_cmp recovered by the fp32 radial backward), stress median 0.99996, all misses the bf16 energy floor under chemical accuracy. Performance: up to 5.5x a 16-thread CPU at 216 atoms; master\047s kernel work restores the trace path at scale and reaches the 512-atom cell in eager.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@janweinreich

Copy link
Copy Markdown
Author

Rebased onto current master (conflict-free — it merges cleanly; PR now shows mergeable) and re-validated end to end: full 757-system A/B vs fairchem gives no genuine force errors (741/757 force PCC > 0.999) and 691/757 pass the strict bar, above the 672 the pre-perf-pass backbone reached; up to 5.5× a 16-thread CPU at 216 atoms (master's kernels also restore the trace path at scale). Details in docs/uma-s-1p2-validation.md.

The one real force regression master's bf16 radial-MLP backward introduced (el_Sn_cmp, 230 meV/Å) is fixed here in fp32 (verified vs an fp64 mirror.radial_mlp oracle → PCC 1.0000).

I also ran a max-effort self-review and hardened the new charge-balancing across the batched paths it hadn't reached: MultiCard now builds the per-system mean operator (was a hang on uma-s-1.2), the traced batch uses the true per-system natoms, charged batches of unequal atom counts are rejected with a clear error rather than silently mis-targeting, and the vendored Wigner-D asset ships in the wheel (pyproject package-data, so non-editable installs don't FileNotFoundError).

Still a draft — happy to split the radial-backward fix into its own PR if you'd prefer it separate from the uma-s-1.2 feature.

@janweinreich
janweinreich marked this pull request as ready for review July 10, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants