| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| """K-FAC utilities for multi-device execution.""" |
| import functools |
| from typing import Any, Callable, Sequence |
|
|
| import jax |
| from jax import lax |
| import jax.numpy as jnp |
| from kfac_jax._src.utils import types |
|
|
| try: |
| |
| from jax.extend.core import unsafe_get_axis_names_DO_NOT_USE |
| except ImportError: |
| |
| from jax.core import unsafe_get_axis_names_DO_NOT_USE |
|
|
| jax_version = ( |
| jax.__version_info__ if hasattr(jax, "__version_info__") |
| else tuple(map(int, jax.__version__.split(".")))) |
|
|
|
|
| Array = types.Array |
| Numeric = types.Numeric |
| PRNGKey = types.PRNGKey |
| TArrayTree = types.TArrayTree |
|
|
|
|
| def _axis_name_tuple(axis_name): |
| if axis_name is None: |
| return () |
| if isinstance(axis_name, tuple): |
| return axis_name |
| return (axis_name,) |
|
|
|
|
| def in_pmap(axis_name: str | tuple[str, ...] | None) -> bool: |
| """Returns whether we are in a pmap with the given axis name.""" |
|
|
| if axis_name is None: |
| return False |
|
|
| axis_names = unsafe_get_axis_names_DO_NOT_USE() |
| requested = _axis_name_tuple(axis_name) |
|
|
| if all(name in axis_names for name in requested): |
| return True |
|
|
| if len(axis_names) > 0: |
| raise ValueError( |
| f"In pmap with axis names {axis_names}, but wrong axis name " |
| f"({axis_name}) was provided. This is likely a bug." |
| ) |
|
|
| return False |
|
|
|
|
| def wrap_if_pmap( |
| p_func: Callable[[TArrayTree, str], TArrayTree], |
| ) -> Callable[[TArrayTree, str | None], TArrayTree]: |
| """Wraps `p_func` to be executed only when inside a `jax.pmap` context.""" |
|
|
| @functools.wraps(p_func) |
| def p_func_if_pmap(obj: TArrayTree, axis_name: str | None) -> TArrayTree: |
| return p_func(obj, axis_name) if in_pmap(axis_name) else obj |
|
|
| return p_func_if_pmap |
|
|
|
|
| |
| |
| def pmean_if_pmap(obj: TArrayTree, axis_name: str | None) -> TArrayTree: |
| return lax.pmean(obj, axis_name) if in_pmap(axis_name) else obj |
|
|
|
|
| def psum_if_pmap(obj: TArrayTree, axis_name: str | None) -> TArrayTree: |
| return lax.psum(obj, axis_name) if in_pmap(axis_name) else obj |
|
|
|
|
| pmap_mean = jax.pmap(lambda x: lax.pmean(x, "i"), axis_name="i") |
| pmap_sum = jax.pmap(lambda x: lax.psum(x, "i"), axis_name="i") |
|
|
|
|
| def is_scalar(x: Any) -> bool: |
| return isinstance(x, (float, int)) or ( |
| isinstance(x, jax.Array) and not x.shape |
| ) |
|
|
|
|
| def using_legacy_pmap() -> bool: |
| """Returns whether the legacy pmap is being used.""" |
| return False |
|
|
|
|
| def get_device_n_contents(obj: TArrayTree, n: int) -> TArrayTree: |
| """Gets the contents from pmap output for device n.""" |
|
|
| def _get_device_n_contents(value: Numeric) -> Numeric: |
|
|
| if is_scalar(value): |
| return value |
|
|
| if using_legacy_pmap(): |
| return value[n] |
|
|
| assert isinstance(value, jax.Array) |
|
|
| if isinstance(value.sharding, jax.sharding.SingleDeviceSharding): |
| return value[n] |
|
|
| assert isinstance(value.sharding, jax.NamedSharding) |
|
|
| shard_data = value.addressable_shards[n].data |
| if value.sharding.spec[0] is None: |
| return shard_data |
|
|
| return shard_data.squeeze(0) |
|
|
| return jax.tree_util.tree_map(_get_device_n_contents, obj) |
|
|
|
|
| def get_first(obj: TArrayTree) -> TArrayTree: |
| return get_device_n_contents(obj, 0) |
|
|
|
|
| def get_mean(obj: TArrayTree) -> TArrayTree: |
| """Returns the average of `obj` over different devices.""" |
| return get_first(pmap_mean(obj)) |
|
|
|
|
| def get_sum(obj: TArrayTree) -> TArrayTree: |
| """Returns the sum of `obj` over different devices.""" |
| return get_first(pmap_sum(obj)) |
|
|
|
|
| _broadcast_all_local_devices_legacy = jax.pmap(lambda x: x) |
| _broadcast_all_local_devices_cache: dict[ |
| str | None, Callable[[TArrayTree], TArrayTree] |
| ] = {} |
|
|
|
|
| def broadcast_all_local_devices( |
| obj: TArrayTree, axis_name: str | None = None |
| ) -> TArrayTree: |
| """Broadcasts `obj` to all local Jax devices. |
| |
| Args: |
| obj: A pytree to broadcast. |
| axis_name: Optional axis name for the pmap. |
| |
| Returns: |
| The broadcasted pytree. |
| """ |
| if types.tree_is_empty(obj): |
| return obj |
|
|
| |
| if axis_name is None: |
| return _broadcast_all_local_devices_legacy(obj) |
|
|
| devices = jax.local_devices() |
| mesh = jax.sharding.Mesh(devices, (axis_name,)) |
| sharding = jax.NamedSharding(mesh, jax.sharding.PartitionSpec(axis_name)) |
|
|
| def _broadcast_with_axis(x): |
| return jax.device_put(x, sharding) |
|
|
| return jax.tree_util.tree_map(_broadcast_with_axis, obj) |
|
|
|
|
| pmap_zeros_like = jax.pmap(lambda x: jax.tree_util.tree_map(jnp.zeros_like, x)) |
| jit_zeros_like = jax.jit(lambda x: jax.tree_util.tree_map(jnp.zeros_like, x)) |
|
|
|
|
| def replicate_all_local_devices( |
| obj: TArrayTree, axis_name: str | None = None |
| ) -> TArrayTree: |
| """Replicates `obj` to all local Jax devices. |
| |
| Args: |
| obj: A pytree to replicate. |
| axis_name: Optional axis name for sharding. When the result will be passed |
| to a pmap with a specific axis_name, this should match to avoid mesh |
| sharding mismatches. |
| |
| Returns: |
| The replicated pytree. |
| """ |
| if types.tree_is_empty(obj): |
| return obj |
|
|
| devices = jax.local_devices() |
|
|
| |
| if axis_name is None: |
| return jax.device_put_replicated(obj, devices=devices) |
|
|
| mesh = jax.sharding.Mesh(devices, (axis_name,)) |
| sharding = jax.NamedSharding(mesh, jax.P(axis_name)) |
|
|
| def _replicate_with_axis(x): |
| |
| stacked = jnp.stack([x] * len(devices)) |
| return jax.device_put(stacked, sharding) |
|
|
| return jax.tree_util.tree_map(_replicate_with_axis, obj) |
|
|
|
|
| def make_different_rng_key_on_all_devices(rng: PRNGKey) -> PRNGKey: |
| """Makes a different PRNG for all Jax devices and processes.""" |
|
|
| rng = jax.random.fold_in(rng, jax.process_index()) |
| rng = jax.random.split(rng, jax.local_device_count()) |
|
|
| return broadcast_all_local_devices(rng) |
|
|
|
|
| p_split = jax.pmap(lambda key: tuple(jax.random.split(key))) |
|
|
| p_split_num = jax.pmap(lambda key, num: tuple(jax.random.split(key, num)), |
| static_broadcasted_argnums=1) |
|
|
|
|
| default_device_sync = None |
|
|
|
|
| def host_sync( |
| obj: TArrayTree, |
| sync_op: Callable[[TArrayTree, str], TArrayTree], |
| ) -> TArrayTree: |
| """Syncs `obj` across multiple hosts with the operation `sync_op`.""" |
|
|
| |
| |
| |
|
|
| if jax.process_count() > 1: |
|
|
| |
| |
|
|
| global default_device_sync |
|
|
| if default_device_sync is None: |
|
|
| default_devices = [jax.local_devices(process_index=p_idx)[0] |
| for p_idx in range(jax.process_count())] |
|
|
| default_device_sync = jax.pmap(lambda x, sync_op: sync_op(x, "i"), |
| devices=default_devices, |
| axis_name="i", |
| static_broadcasted_argnums=1) |
|
|
| obj = jax.tree_util.tree_map(lambda x: jnp.expand_dims(x, axis=0), obj) |
|
|
| return get_first(default_device_sync(obj, sync_op)) |
|
|
| return obj |
|
|
|
|
| def host_all_gather(x: TArrayTree) -> TArrayTree: |
| """Gathers on every host the values of the PyTree leaves `x`.""" |
| return host_sync(x, lax.all_gather) |
|
|
|
|
| def host_mean(x: TArrayTree) -> TArrayTree: |
| """Computes the mean of the PyTree leaves of `x` over multiple hosts.""" |
| return host_sync(x, lax.pmean) |
|
|
|
|
| def sync_and_divide_value( |
| value: TArrayTree, |
| counter: Numeric, |
| axis_name: str | None = None, |
| ) -> TArrayTree: |
| """Computes the mean of `value` over all hosts and divides it by `counter`.""" |
| value = jax.tree_util.tree_map(lambda x: x / counter, value) |
| return pmean_if_pmap(value, axis_name) |
|
|
|
|
| jit_sync_and_divide_value = jax.jit(sync_and_divide_value) |
| pmap_sync_and_divide_value = jax.pmap( |
| functools.partial(sync_and_divide_value, axis_name="i"), |
| axis_name="i", |
| ) |
|
|
|
|
| |
| |
| def copy_array(x: Array) -> Array: |
| """Copies a Jax array so that it can be donated freely.""" |
| return x + jnp.zeros_like(x) |
|
|
|
|
| copy_obj = jax.jit(lambda x: jax.tree_util.tree_map(copy_array, x)) |
| _pmap_copy_obj = jax.pmap(copy_obj) |
|
|
|
|
| def pmap_copy_obj(x: TArrayTree | None) -> TArrayTree | None: |
|
|
| |
| if x is None: |
| return None |
|
|
| if types.tree_is_empty(x): |
| |
| (flattened, structure) = jax.tree_util.tree_flatten(x) |
| return jax.tree_util.tree_unflatten(structure, flattened) |
|
|
| return _pmap_copy_obj(x) |
|
|
|
|
| def distribute_thunks( |
| thunks: Sequence[Callable[[], TArrayTree]], |
| pmap_axis_name: str, |
| ) -> TArrayTree: |
| """Distributes the computation of a list of thunks over the pmapped devices. |
| |
| Given a list of thunks, this function distributes their computation over the |
| devices of the current pmap in a round-robin fashion, syncronizes the results |
| across devices, and then returns them as a sequence of PyTrees. |
| |
| Note that this function is meant to be used in a compiled context, and may |
| call ``thunk[i]()`` several times for each i, with all but one call getting |
| "optimized away" by XLA. |
| |
| Args: |
| thunks: A sequence of callables performing the desired computations. Each |
| callable must take zero arguments and return a PyTree of JAX arrays. As |
| with callables passed to (most) standard JAX API functions, these need to |
| be stateless and free of side effects. The output of each callable must be |
| the same regardless of the device it is executed on. |
| pmap_axis_name: The name of the pmap axis to use. |
| |
| Returns: |
| A sequence of PyTrees that are the output of the corresponding element of |
| ``thunks``. |
| """ |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| if not in_pmap(pmap_axis_name): |
| raise ValueError(f"Provided pmap_axis_name {pmap_axis_name} is not a valid " |
| "pmap axis in current pmap (or this function was not " |
| "called in a pmap).") |
|
|
| assert pmap_axis_name is not None |
|
|
| axis_names = _axis_name_tuple(pmap_axis_name) |
| total_devices = lax.psum(1, axis_name=pmap_axis_name) |
| if len(axis_names) == 1: |
| current_device_index = lax.axis_index(axis_names[0]) |
| else: |
| |
| |
| current_device_index = 0 |
| stride = 1 |
| for axis in reversed(axis_names): |
| current_device_index = current_device_index + lax.axis_index(axis) * stride |
| stride = stride * lax.psum(1, axis_name=axis) |
|
|
| |
| dummy_output_trees = tuple(thunk() for thunk in thunks) |
|
|
| def make_branch(device_index): |
|
|
| def branch(): |
| """Execute only thunks i such that i % total_devices == device_index.""" |
|
|
| outs = [] |
| for i in range(len(thunks)): |
|
|
| if i % total_devices == device_index: |
| outs.append(thunks[i]()) |
| else: |
| outs.append( |
| jax.tree_util.tree_map(jnp.zeros_like, dummy_output_trees[i])) |
|
|
| return tuple(outs) |
|
|
| return branch |
|
|
| branches = tuple(make_branch(device_index) |
| for device_index in range(total_devices)) |
|
|
| output_trees = jax.lax.switch(current_device_index, branches) |
|
|
| return jax.lax.psum(output_trees, axis_name=pmap_axis_name) |
|
|