MACE PR draft: reassociated first interaction¶
Upstream: ACEsuit/mace, base main at e37c8583a8b1b586e5888fa6dca86e8f591e2606 (19 October 2026).
Patch: 0001-reassociate-first.patch (new files only:
mace/modules/reassociate.py, tests/unit/test_reassociate.py). Not opened (owner opens it, decision
D5 (b)).
Title¶
Add an inference-time reassociation of the first interaction: sum edges per (receiver, sender species) before the radial projection
Description¶
Motivation¶
In the first interaction block the sender features are species-only scalars
(linear_up(node_embedding(one_hot))), and the convolution is channelwise with one radial weight per
(path, channel). Today every edge forms its [P*C] tensor-product weights (conv_tp_weights, e.g.
4 x 128 = 512 values per edge) and the tensor product, then sums per receiver. Because the last radial
layer is affine and the sender features depend only on the sender's species, the sums can be
reassociated:
m[i, l, m, u] = gamma_l sum_z x[z, u] sum_k W[path(l)·C + u, k] S[i, z, k, (l, m)]
S[i, z, k, (l, m)] = sum_{edges j→i, species(j)=z} a[e, k] Y[e, (l, m)], a[e] = [h_e c_e, c_e]
S is a small edge sum over K ≈ 65 hidden radial features (plus a bias column), and the projection
is one dense GEMM per path over [atoms·(2l+1), species·K]. The per-edge weights are never formed.
The conversion reassociate_first_interaction(model, species_columns=None) needs no change to any
interaction forward: it swaps conv_tp_weights for "hidden layers, then append a 1 column" (the
forward's existing * cutoff produces a), and conv_tp for a module with the fused calling
convention (conv_fusion), which gets node_attrs from a forward pre-hook. gamma_l and the output
layout are measured from the model's own conv_tp (e3nn, cuEquivariance or OpenEquivariance), so no
normalisation convention is assumed. The conversion returns False and leaves the model unchanged
where the form does not hold (non-scalar sender irreps, non-channelwise TP, non-affine last radial layer,
models with joint_embedding).
Measured gains (FastMACE / research-engine implementation of the same algorithm)¶
| Model | Change | NaCl | NaNbO3 | NbCl5 | GPU | Source |
|---|---|---|---|---|---|---|
| MACE-MH-1 | plain PyTorch prototype (as in this patch) | −0.6 to −2.6 % | RTX 5060 Ti | ResearchModule docs/roadmap.md item 11.2, 10 October 2026 |
||
| MACE-MH-1 | with a fused Triton backward of the edge sum | −4.9 % | −7.2 % | −4.9 % | RTX 5060 Ti | same |
| MP-0, MPA-0, OMAT-0, MatPES and other non-MH-1 benchmark models | with the receiver-streaming convolution, together | −16 to −28 % | RTX 5060 Ti | docs/roadmap.md item 13, studies/other_models/README.md, 15 October 2026 |
||
| MACE-OMOL-0 | reassociation added | −9 % | RTX 5060 Ti | AGENTS.md / studies/other_models/README.md, 16 October 2026 |
MD-step times, float32 hybrid, CUDA graphs, 3x3x3 cells. All passed the force and graph-replay gates.
FastMACE: records reassociate_first (MH-1 family, fastmace/rewrites/mh1/) and generic_reassociate
(fastmace/rewrites/generic/); float64 agreement 1.3e-16 to 3.2e-15 eV/Å on mp0-medium
(docs/validation/1a-T08-generic-rewrites.md). Its edge-sum cost after the rewrite is 0.06 ms (NaCl) to
0.5 ms (NbCl5) per force call (docs/validation/2-T20a-step-breakdown.md).
Honest expectation for this patch: it is the plain PyTorch form (an index_add_ of outer products,
autograd backward). Our measurements say most of the gain needs a fused backward of the edge sum; this
PR is the correct, general base on which such a kernel can be added later.
Exactness¶
The same products in a different summation order: exact up to rounding. The float64 test agrees with the
unconverted model to 1e-10 eV (energy) and 1e-9 eV/Å (forces) on Residual, DensityResidual and
ResidualNonLinear first blocks. index_add_ on CUDA uses atomics: not bitwise reproducible on GPU (as
MACE's scatter_sum already is).
Limitations¶
- inference only (weights frozen into a buffer);
- memory of
Sisatoms x species x (lmax+1)^2 x (K+1); passspecies_columns(the elements a simulation can contain) for 89-element foundation models; - the converted model is not TorchScript-exportable (forward pre-hook with kwargs, Python state);
- the species table
xis read back fromnode_featswithindex_reduce_(amax), which assumes that every atom of a species has identical first-layer features (true for species-only embeddings; models withjoint_embeddingare refused).
Test plan¶
- [x]
tests/unit/test_reassociate.py: float64 three-species MACE models with each residual first block, energy and forces against the unconverted model, with and withoutspecies_columns. Run: 6 passed on MACE 0.3.16 / torch 2.14.1+cpu (MACE main itself not run: Python 3.10 environment). - [ ] maintainers' CI on main.
- [ ] GPU:
mace-mp-0medium and MH-1 withenable_cueqandenable_oeq(theconv_fusionprobe path), float32 force check, MD-step timing. - [ ] a model whose
conv_tp_weightsis e3nn'sFullyConnectedNetwith normalised weights (affinity probe covers it; add an explicit test).
Risks¶
- Small or no gain without a fused backward, as measured (0.6–2.6 %). Reviewers may prefer to wait for a kernel.
- Probing relies on internal structure (
interaction.edge_irreps,edge_attrs_irreps,conv_tp.weight_numel, radial net layer list). Any mismatch returnsFalserather than producing wrong numbers, and the affinity of the last radial layer is checked numerically. - Pre-hook state (
conv_tp.node_attrs) is per call; concurrent calls of one model object from two threads would race (MACE does not do this).