Skip to content

Commit 51e0995

Browse files
bkmirayg1234
andauthored
Uma base inference speedups (#2111)
Speed up UMA inference and DDP training without changing numerics (updated Aug 3rd) # perf(inference): freeze prepared model parameters ## Summary Freeze model parameters after inference-specific module preparation. Inference may differentiate outputs with respect to positions and cells for forces, stress, and Hessians, but it does not need parameter gradients. Freezing after MOLE and backend replacement avoids retaining unnecessary weight-gradient state while preserving input derivatives. The policy now covers both `general` and `umas_fast_gpu`. Main's folded-batch linear path (`9547a5b3c`) removes the former general-backend frozen-weight regression. Prediction retains the full `AtomicData.clone()` boundary because graph parallelism, MOLE preparation, and conservative gradients can mutate model inputs. This PR also adds frozen/unfrozen energy, force, stress, and Hessian parity coverage for both execution modes, singleton graph-parallel regression coverage, and pretrained-model test routing markers. ## Performance Compared current `origin/main` at `9547a5b3c` with the parameter-freezing change. Both sides include the new fused Wigner/SO2 edgewise kernels and folded-batch linear operations. All GPU runs used UMA-S-1.2.1 (`uma-s-1p2p1`), PyTorch 2.13.0+cu130, H100 80 GB HBM3 GPUs, and the `h100_ocp_high` QoS. ### Fast GPU backend Each atom count is one global FCC structure partitioned across a graph-parallel group containing every allocated GPU; atom counts are not per GPU. The primary throughput metric is median synchronized QPS. Peak allocated memory is the maximum rank value; total allocated memory is summed across ranks. | GPUs | Atoms | Main QPS | PR QPS | Speedup | Allocated GiB/GPU, main -> PR | Allocated delta | Reserved delta | Total allocated GiB, main -> PR | | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | 8 | 100 | 30.60 | 32.43 | 1.060x | 0.18 -> 0.17 | -8.9% | -3.0% | 1.44 -> 1.31 | | 8 | 1,000 | 29.53 | 32.05 | 1.086x | 0.88 -> 0.70 | -20.6% | -20.3% | 7.06 -> 5.60 | | 8 | 10,000 | 21.08 | 23.08 | 1.095x | 7.32 -> 5.64 | -22.9% | -21.0% | 58.48 -> 45.08 | | 32 | 100 | 26.86 | 28.42 | 1.058x | 0.13 -> 0.13 | -2.9% | -5.2% | 4.10 -> 4.00 | | 32 | 1,000 | 26.17 | 27.58 | 1.054x | 0.33 -> 0.27 | -17.2% | -13.0% | 10.41 -> 8.63 | | 32 | 10,000 | 25.97 | 27.05 | 1.042x | 2.15 -> 1.63 | -24.0% | -21.7% | 68.19 -> 51.97 | Freezing remains beneficial after the fused fast-backend kernels: median QPS improves in all six cases by 4.2-9.5%, while the 10,000-atom cases save 23-24% peak allocated memory and 21-22% peak reserved memory. The fast-backend benchmark used candidate snapshot `a2dca9379`. Its fast path is behaviorally identical to final commit `3af44001e`; the final commit only extends the same freeze operation to other execution modes and broadens tests. ### General backend The general-backend check used one H100 without graph-parallel initialization. It applies freezing after MOLE preparation and before compilation, exactly where the final implementation applies it. | Atoms | Main QPS | Frozen QPS | Speedup | Allocated GiB, main -> frozen | Allocated delta | Reserved delta | | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | 100 | 47.53 | 55.52 | 1.168x | 1.04 -> 0.76 | -27.0% | -23.4% | | 1,000 | 16.98 | 19.55 | 1.151x | 11.80 -> 8.36 | -29.1% | -25.0% | | 2,000 | 10.24 | 11.76 | 1.149x | 21.40 -> 15.16 | -29.2% | -25.1% | The folded-batch linear change reverses the former general-backend regression: freezing is now 14.9-16.8% faster and saves 27-29% allocated memory. A local 16-thread CPU check with the general backend and compilation disabled measured `1.040x` at 32 atoms and `1.002x` at 1,000 atoms. The new path is positive for the small case and neutral for the larger case, rather than the previous CPU slowdown. ## Methodology - Fast settings: FP32 base precision, TF32 enabled, `umas_fast_gpu`, `merge_mole=True`, `compile=True`, and `activation_checkpointing=False`. - General settings: the same settings with `execution_mode=general`; the one-GPU check did not initialize graph parallelism. - Order on each GPU allocation: main A, candidate A, candidate B, main B (ABBA). - Each GPU branch/size result: 5 warmups followed by 3 repeats of 10 predictions. - Reported QPS: median of 6 synchronized repeats across the two runs. - Multi-GPU timing: maximum elapsed time across ranks after CUDA synchronization and a distributed barrier. - Memory: CUDA peaks reset after warmup; maximum-rank and aggregate allocated and reserved memory were recorded. - Inputs: the same seed-42 FCC structures were used by every branch and GPU count; all structure checksums match exactly. ## Fidelity Energy, force, and stress checksums were captured for every run. Across the general and fast comparisons, the maximum main-to-frozen energy-sum relative difference was below `4.5e-5`, the maximum-force checksum absolute difference was `2.54e-4`, and the maximum-stress checksum absolute difference was `3.24e-6`. These differences are on the same scale as repeated distributed GPU runs of the same commit. Focused regression tests compare frozen and unfrozen energy, force, stress, and Hessian tensors for both execution modes. The fast backend uses `hessian_vmap=False` because its custom backward operators do not implement `vmap` batching. ## Validation - Pre-commit passed for every modified file after merging `origin/main`. - `tests/core/common/test_gp_utils.py`: 18 passed on PyTorch 2.13. - `test_untrained_hessian_cpu`: passed with all model parameters frozen. - Frozen/unfrozen GPU derivative parity: 2 passed (`general` and `umas_fast_gpu`) in Slurm job `9976221`. - General one-GPU ABBA benchmark: completed without failures in job `9974069`. - Fast 8- and 32-GPU ABBA benchmarks: completed without failures in jobs `9973417` and `9973418`. - `graph_parallel_group_size=None` remains the no-GP path; group size 1 remains an intentional singleton-GP test path. --------- Co-authored-by: Ray Gao <7001989+rayg1234@users.noreply.github.com>
1 parent 9547a5b commit 51e0995

5 files changed

Lines changed: 126 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,32 @@ configs/ # Hydra YAML configs (datasets, tasks, backbone, optimi
242242
- `lmdb` - Dataset storage format
243243
- `ray[serve]>=2.53.0` - Distributed computing
244244
245+
## Testing Gotchas
246+
247+
- Tests that download registered checkpoints must declare their models with a
248+
`pretrained` marker. This lets base CI deselect them with `--exclude-models`
249+
and routes them to the matching model-sweep job.
250+
- Freeze inference parameters after inference-specific module replacement.
251+
Main's folded-batch linear path removes the former general-backend regression:
252+
on one H100, freezing improved compiled general inference by 15-17% and cut
253+
peak allocated memory by 27-29% at 100-2,000 atoms. PyTorch 2.13 CPU checks
254+
improved by 4% at 32 atoms and were neutral at 1,000 atoms. Custom backward
255+
paths must preserve input derivatives independently of parameter gradients.
256+
- `umas_fast_gpu` custom backward operators do not implement `vmap` batching.
257+
Compute Hessians through the per-component loop (`hessian_vmap=False`) when
258+
exercising that backend, and ensure inference settings forward that option
259+
into the backbone configuration.
260+
- Set `CI=true` when reproducing CPU CI shards locally. Some multi-GPU graph
261+
parallel tests rely on that environment variable for skipping instead of the
262+
`gpu` marker, so the CI marker expression alone will still collect them.
263+
- `graph_parallel_group_size=None` disables graph-parallel setup. A value of
264+
`1` intentionally initializes singleton graph- and data-parallel groups and
265+
is used to exercise those paths in tests; do not treat it as disabled.
266+
- Keep the full `AtomicData.clone()` boundary in prediction unless benchmarks
267+
justify changing it and every model-side mutation has been audited. Graph
268+
parallelism, MOLE preparation, and conservative gradients can replace or
269+
mutate input fields, so a selective shallow copy is brittle.
270+
245271
## Numerical Precision
246272
247273
- Model constructors must not mutate process-wide PyTorch precision settings

src/fairchem/core/units/mlip_unit/predict.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@
3737
)
3838
from fairchem.core.components.batch_server import get_app_handle_with_retry
3939
from fairchem.core.datasets.atomic_data import AtomicData, warn_if_upcasting
40-
from fairchem.core.models.uma.nn.execution_backends import (
41-
maybe_update_settings_backend,
42-
)
40+
from fairchem.core.models.uma.nn.execution_backends import maybe_update_settings_backend
4341
from fairchem.core.units.mlip_unit import InferenceSettings
4442
from fairchem.core.units.mlip_unit.mlip_unit import OutputSpec, Task
4543
from fairchem.core.units.mlip_unit.single_atom_patch import (
@@ -461,6 +459,8 @@ def _lazy_init(self, data: AtomicData) -> None:
461459
"""
462460
# Model handles its own preparation (MOLE merge, eval mode, etc.)
463461
self.model.module.prepare_for_inference(data, self.inference_settings)
462+
# Inference differentiates outputs with respect to inputs, not weights.
463+
self.model.requires_grad_(False)
464464

465465
self.model.to(self.inference_settings.base_precision_dtype)
466466

tests/core/common/test_gp_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ def _dummy_call(x):
3131
return x
3232

3333

34+
def _group_state():
35+
return (
36+
gp_utils.initialized(),
37+
gp_utils.get_gp_world_size(),
38+
gp_utils.get_dp_world_size(),
39+
)
40+
41+
42+
def test_graph_parallel_size_one_initializes_groups():
43+
config = PGConfig(backend="gloo", world_size=1, gp_group_size=1, use_gp=True)
44+
45+
output = spawn_multi_process(config, _group_state, init_pg_and_rank_and_launch_test)
46+
47+
assert output == [(True, 1, 1)]
48+
49+
3450
@pytest.mark.parametrize(
3551
"world_size, input, expected_output",
3652
[(1, 5, [5]), (3, 0, [0, 0, 0])],

tests/core/components/test_uma_speed_benchmark.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
import tempfile
1111
from pathlib import Path
1212

13+
import pytest
14+
1315
from tests.core.testing_utils import launch_main
1416

17+
pytestmark = pytest.mark.pretrained("uma-s-1p2")
18+
1519
COMMON_ARGS = [
1620
"job.device_type=CPU",
1721
"runner.timeiters=1",

tests/core/units/mlip_unit/test_predict.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,10 @@ def _test_untrained_hessian(checkpoint_path, device):
17961796
# Get predictions
17971797
preds = predictor.predict(batch)
17981798

1799+
assert all(
1800+
not parameter.requires_grad for parameter in predictor.model.parameters()
1801+
)
1802+
17991803
# Verify energy, forces, and hessian are present
18001804
assert "energy" in preds, "Energy prediction missing"
18011805
assert "forces" in preds, "Forces prediction missing"
@@ -1816,6 +1820,77 @@ def _test_untrained_hessian(checkpoint_path, device):
18161820
assert torch.allclose(hessian, hessian.T, atol=1e-5), "Hessian is not symmetric"
18171821

18181822

1823+
@pytest.mark.gpu()
1824+
@pytest.mark.parametrize("execution_mode", ["general", "umas_fast_gpu"])
1825+
def test_frozen_parameters_preserve_input_derivatives(
1826+
conserving_mole_checkpoint, monkeypatch, execution_mode
1827+
):
1828+
_test_frozen_parameters_preserve_input_derivatives(
1829+
conserving_mole_checkpoint[0], monkeypatch, execution_mode
1830+
)
1831+
1832+
1833+
def _test_frozen_parameters_preserve_input_derivatives(
1834+
checkpoint_path, monkeypatch, execution_mode
1835+
):
1836+
settings = InferenceSettings(
1837+
predict_untrained_forces={"omol"},
1838+
predict_untrained_stress={"omol"},
1839+
predict_untrained_hessian={"omol"},
1840+
activation_checkpointing=False,
1841+
merge_mole=True,
1842+
execution_mode=execution_mode,
1843+
hessian_vmap=False,
1844+
)
1845+
1846+
seed_everywhere(42)
1847+
frozen_predictor = MLIPPredictUnit(
1848+
checkpoint_path, device="cuda", inference_settings=settings
1849+
)
1850+
seed_everywhere(42)
1851+
unfrozen_predictor = MLIPPredictUnit(
1852+
checkpoint_path, device="cuda", inference_settings=settings
1853+
)
1854+
monkeypatch.setattr(
1855+
unfrozen_predictor.model,
1856+
"requires_grad_",
1857+
lambda requires_grad=True: unfrozen_predictor.model,
1858+
)
1859+
1860+
def make_batch():
1861+
atoms = molecule("H2O")
1862+
atoms.info.update({"charge": 0, "spin": 1})
1863+
data = AtomicData.from_ase(
1864+
atoms,
1865+
task_name="omol",
1866+
r_data_keys=["spin", "charge"],
1867+
molecule_cell_size=120,
1868+
)
1869+
return atomicdata_list_to_batch([data])
1870+
1871+
seed_everywhere(42)
1872+
frozen = frozen_predictor.predict(make_batch())
1873+
seed_everywhere(42)
1874+
unfrozen = unfrozen_predictor.predict(make_batch())
1875+
1876+
assert all(
1877+
not parameter.requires_grad for parameter in frozen_predictor.model.parameters()
1878+
)
1879+
assert any(
1880+
parameter.requires_grad for parameter in unfrozen_predictor.model.parameters()
1881+
)
1882+
assert frozen.keys() == unfrozen.keys()
1883+
for name in frozen:
1884+
atol = 1e-5 if name == "hessian" else 1e-6
1885+
torch.testing.assert_close(
1886+
frozen[name],
1887+
unfrozen[name],
1888+
rtol=1e-5,
1889+
atol=atol,
1890+
msg=lambda message, name=name: f"{name}: {message}",
1891+
)
1892+
1893+
18191894
@pytest.mark.gpu()
18201895
def test_hessian_activation_checkpointing(conserving_mole_checkpoint):
18211896
"""
@@ -1993,6 +2068,7 @@ def test_execution_mode_not_set_when_conditions_not_met(pretrained_model_name):
19932068
# ---------------------------------------------------------------------------
19942069

19952070

2071+
@pytest.mark.pretrained("uma-s-1p1")
19962072
def test_uma_1p1_predict_unit_has_model_id():
19972073
"""UMA 1.1 checkpoints have no `model_id` on disk; the compat fixup
19982074
back-fills it to `"UMA-1.1"` at load time."""
@@ -2008,6 +2084,7 @@ def test_uma_1p1_predict_unit_has_model_id():
20082084
assert pu.model.module.backbone.model_id == UMA_1P1_MODEL_ID
20092085

20102086

2087+
@pytest.mark.pretrained("uma-s-1p1")
20112088
def test_uma_1p1_finetune_propagates_model_id():
20122089
"""When finetuning starts from UMA 1.1, the back-filled `model_id` is
20132090
stashed onto `model.finetune_model_full_config` (the fixup runs inside

0 commit comments

Comments
 (0)