Skip to content

Commit 95c1f2e

Browse files
committed
Replace JAX multi-device pmap execution with shard_map and add coverage for sharded training, evaluation, and prediction paths
1 parent c119726 commit 95c1f2e

4 files changed

Lines changed: 255 additions & 47 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ print(atoms.get_potential_energy())
655655

656656
### JAX Backend Multi-Device Notes
657657

658-
- When the JAX backend detects more than one local accelerator, it automatically switches to a multi-device (`pmap`) execution. In that mode the training and evaluation batch size must be divisible by `jax.local_device_count()` so that each device processes an identical number of graphs.
658+
- When the JAX backend detects more than one local accelerator, it automatically switches to a multi-device (`shard_map`) execution. In that mode the training and evaluation batch size must be divisible by `jax.local_device_count()` so that each device processes an identical number of graphs.
659659
- On single-device machines no extra configuration is required; the backend falls back to the same single-device behaviour that existing scripts expect.
660660

661661
---

‎equitrain/backends/jax_backend.py‎

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,18 @@
3636
)
3737
from equitrain.backends.jax_runtime import ensure_multiprocessing_spawn
3838
from equitrain.backends.jax_utils import (
39+
DEVICE_AXIS_NAME,
40+
DEVICE_AXIS_SPEC,
41+
REPLICATED_SPEC,
3942
ModelBundle,
43+
add_local_device_axis,
4044
batched_iterator,
4145
iter_micro_batches,
4246
load_model_bundle,
47+
remove_local_device_axis,
4348
replicate_to_local_devices,
4449
set_jax_platform,
50+
shard_map_over_local_devices,
4551
supports_multiprocessing_workers,
4652
take_chunk,
4753
unreplicate_from_local_devices,
@@ -400,7 +406,7 @@ def _unreplicate(tree):
400406

401407

402408
def _multi_device_chunk_iterator(loader, device_count: int, *, phase: str, logger):
403-
"""Group per-device micro-batches to feed into ``jax.pmap`` calls."""
409+
"""Group per-device micro-batches for mapped local-device calls."""
404410
micro_iter = iter_micro_batches(loader)
405411
first_chunk = take_chunk(micro_iter, device_count)
406412
if len(first_chunk) < device_count:
@@ -493,20 +499,28 @@ def _build_train_functions(
493499
local_devices = jax.local_devices()
494500

495501
def grad_step(params, batch):
502+
params = remove_local_device_axis(params)
503+
batch = remove_local_device_axis(batch)
496504
(loss, aux), grads = jax.value_and_grad(
497505
loss_fn, has_aux=True, allow_int=True
498506
)(params, batch)
499507
grads = _sanitize_grads(grads, clip_value)
500-
grads = jax.lax.pmean(grads, axis_name='device')
501-
loss = jax.lax.pmean(loss, axis_name='device')
502-
aux = jtu.tree_map(lambda x: jax.lax.pmean(x, axis_name='device'), aux)
503-
return loss, aux, grads
508+
grads = jax.lax.pmean(grads, axis_name=DEVICE_AXIS_NAME)
509+
loss = jax.lax.pmean(loss, axis_name=DEVICE_AXIS_NAME)
510+
aux = jtu.tree_map(
511+
lambda x: jax.lax.pmean(x, axis_name=DEVICE_AXIS_NAME), aux
512+
)
513+
return add_local_device_axis((loss, aux, grads))
504514

505-
grad_step_fn = jax.pmap(
506-
grad_step, axis_name='device', in_axes=(0, 0), devices=local_devices
515+
grad_step_fn = shard_map_over_local_devices(
516+
grad_step,
517+
in_specs=(DEVICE_AXIS_SPEC, DEVICE_AXIS_SPEC),
518+
devices=local_devices,
507519
)
508520

509521
def apply_updates(state: TrainState, grads, ema_factor):
522+
state = remove_local_device_axis(state)
523+
grads = remove_local_device_axis(grads)
510524
updates, new_opt_state = optimizer.update(
511525
grads, state.opt_state, state.params
512526
)
@@ -520,16 +534,15 @@ def apply_updates(state: TrainState, grads, ema_factor):
520534
)
521535
else:
522536
new_ema = state.ema_params
523-
return (
537+
return add_local_device_axis(
524538
TrainState(
525539
params=new_params, opt_state=new_opt_state, ema_params=new_ema
526-
),
540+
)
527541
)
528542

529-
apply_updates_fn = jax.pmap(
543+
apply_updates_fn = shard_map_over_local_devices(
530544
apply_updates,
531-
axis_name='device',
532-
in_axes=(0, 0, None),
545+
in_specs=(DEVICE_AXIS_SPEC, DEVICE_AXIS_SPEC, REPLICATED_SPEC),
533546
devices=local_devices,
534547
)
535548
return grad_step_fn, apply_updates_fn
@@ -565,12 +578,20 @@ def _build_eval_step(loss_fn, *, multi_device: bool):
565578
local_devices = jax.local_devices()
566579

567580
def step(params, batch):
581+
params = remove_local_device_axis(params)
582+
batch = remove_local_device_axis(batch)
568583
loss, aux = loss_fn(params, batch)
569-
loss = jax.lax.pmean(loss, axis_name='device')
570-
aux = jtu.tree_map(lambda x: jax.lax.pmean(x, axis_name='device'), aux)
571-
return loss, aux
584+
loss = jax.lax.pmean(loss, axis_name=DEVICE_AXIS_NAME)
585+
aux = jtu.tree_map(
586+
lambda x: jax.lax.pmean(x, axis_name=DEVICE_AXIS_NAME), aux
587+
)
588+
return add_local_device_axis((loss, aux))
572589

573-
return jax.pmap(step, axis_name='device', in_axes=(0, 0), devices=local_devices)
590+
return shard_map_over_local_devices(
591+
step,
592+
in_specs=(DEVICE_AXIS_SPEC, DEVICE_AXIS_SPEC),
593+
devices=local_devices,
594+
)
574595

575596
return jax.jit(loss_fn)
576597

‎equitrain/backends/jax_predict.py‎

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import functools
43
import logging
54
import pickle
65
import threading
@@ -17,9 +16,15 @@
1716

1817
from equitrain.argparser import check_args_complete
1918
from equitrain.backends.jax_utils import (
19+
DEVICE_AXIS_SPEC,
20+
REPLICATED_SPEC,
21+
add_local_device_axis,
2022
prepare_sharded_batch,
23+
remove_local_device_axis,
2124
set_jax_platform,
22-
split_graphs_for_devices,
25+
shard_graphs_for_devices,
26+
shard_map_over_local_devices,
27+
split_device_outputs,
2328
stack_or_none,
2429
supports_multiprocessing_workers,
2530
)
@@ -48,6 +53,14 @@
4853
}
4954

5055

56+
def _get_global_value(globals_attr, key: str):
57+
if globals_attr is None:
58+
return None
59+
if hasattr(globals_attr, 'items') and key in globals_attr:
60+
return globals_attr[key]
61+
return getattr(globals_attr, key, None)
62+
63+
5164
def _prefetch_to_device(iterator, capacity: int, device_put_fn: Callable[[Any], Any]):
5265
"""Prefetch items onto device(s) using a bounded queue."""
5366
if capacity is None or capacity <= 0:
@@ -97,7 +110,7 @@ def _split_prediction_outputs(
97110
outputs: dict[str, Any],
98111
graph: jraph.GraphsTuple,
99112
) -> tuple[list[int], dict[str, list[Any]]]:
100-
graph_ids = getattr(graph.globals, 'graph_id', None)
113+
graph_ids = _get_global_value(graph.globals, 'graph_id')
101114
if graph_ids is None:
102115
raise ValueError(
103116
'Streaming prediction requires graph.globals.graph_id to be set.'
@@ -197,32 +210,39 @@ def predict_streaming(
197210
except Exception: # pragma: no cover - defensive fallback
198211
total_hint = 0
199212

200-
_predict_step = functools.partial(
201-
jax.pmap,
202-
in_axes=(None, 0),
203-
out_axes=0,
204-
axis_name='devices',
205-
devices=local_devices,
206-
)(predictor)
213+
def _mapped_predictor(params, batch):
214+
batch = remove_local_device_axis(batch)
215+
return add_local_device_axis(predictor(params, batch))
216+
217+
if local_device_count <= 1:
218+
_predict_step = jax.jit(_mapped_predictor)
219+
else:
220+
_predict_step = shard_map_over_local_devices(
221+
_mapped_predictor,
222+
in_specs=(REPLICATED_SPEC, DEVICE_AXIS_SPEC),
223+
devices=local_devices,
224+
)
207225

208226
def _prepare_device_graphs(graph):
209227
if local_device_count <= 1:
210228
if graph.n_node.ndim == 1:
211229
device_batch = jax.tree_util.tree_map(lambda x: x[None, ...], graph)
212-
else:
213-
device_batch = graph
214-
return device_batch, [graph]
230+
return device_batch, [graph]
231+
device_batch = graph
232+
device_graphs = split_device_outputs(graph, local_device_count)
233+
return device_batch, device_graphs
215234
if graph.n_node.ndim == 1:
216235
device_batch = prepare_sharded_batch(graph, local_device_count)
217-
else:
218-
if graph.n_node.shape[0] != local_device_count:
219-
raise ValueError(
220-
'Expected microbatches with leading axis equal to the number of '
221-
f'local devices ({local_device_count}), got axis size '
222-
f'{graph.n_node.shape[0]}.'
223-
)
224-
device_batch = graph
225-
device_graphs = split_graphs_for_devices(graph, local_device_count)
236+
device_graphs = shard_graphs_for_devices(graph, local_device_count)
237+
return device_batch, device_graphs
238+
if graph.n_node.shape[0] != local_device_count:
239+
raise ValueError(
240+
'Expected microbatches with leading axis equal to the number of '
241+
f'local devices ({local_device_count}), got axis size '
242+
f'{graph.n_node.shape[0]}.'
243+
)
244+
device_batch = graph
245+
device_graphs = split_device_outputs(graph, local_device_count)
226246
return device_batch, device_graphs
227247

228248
host_prefetch_cap = int(getattr(data_loader, '_prefetch_batches', 0) or 0)

0 commit comments

Comments
 (0)