from functools import reduce, wraps
from typing import Any, Callable, Optional
import mlx.core as mx
from ..utils import tree_flatten, tree_map, tree_reduce, tree_unflatten
from .layers.base import Module
def value_and_grad(model: Module, fn: Callable):
def inner_fn(params, *args, **kwargs):
model.update(params)
return fn(*args, **kwargs)
value_grad_fn = mx.value_and_grad(inner_fn)
@wraps(fn)
def wrapped_value_grad_fn(*args, **kwargs):
value, grad = value_grad_fn(model.trainable_parameters(), *args, **kwargs)
return value, grad
return wrapped_value_grad_fn
def checkpoint(module: Module, fn: Optional[Callable] = None):
if fn is None:
fn = module
def inner_fn(params, *args, **kwargs):
module.update(params)
return fn(*args, **kwargs)
checkpointed_fn = mx.checkpoint(inner_fn)
@wraps(fn)
def wrapped_checkpointed_fn(*args, **kwargs):
return checkpointed_fn(module.trainable_parameters(), *args, **kwargs)
return wrapped_checkpointed_fn
def _extract_info(flat):
keys = [k for k, _ in flat]
shapes = [g.shape for _, g in flat]
sizes = [g.size for _, g in flat]
dtypes = [g.dtype for _, g in flat]
return keys, shapes, sizes, dtypes
def _group_by_size(keys, sizes, itemsize, communication_size):
grad_groups = []
grad_group = []
grad_group_size = 0
for i in range(len(keys)):
grad_group.append(i)
grad_group_size += sizes[i] * itemsize
if grad_group_size >= communication_size:
grad_groups.append(grad_group)
grad_group = []
grad_group_size = 0
if grad_group:
grad_groups.append(grad_group)
grad_group = []
return grad_groups
def average_gradients(
gradients: Any,
group: Optional[mx.distributed.Group] = None,
all_reduce_size: int = 32 * 1024**2,
communication_stream: Optional[mx.Stream] = None,
):
group = group or mx.distributed.init()
N = group.size()
if N == 1:
return gradients
if all_reduce_size <= 0:
return tree_map(
lambda x: mx.distributed.all_sum(
x,
group=group,
stream=communication_stream,
)
/ N,
gradients,
)
else:
flat_grads = tree_flatten(gradients)
if len(flat_grads) == 0:
return gradients
keys, shapes, sizes, dtypes = _extract_info(flat_grads)
if not all(dt == dtypes[0] for dt in dtypes):
return average_gradients(gradients, group, 0)
grad_groups = _group_by_size(keys, sizes, dtypes[0].size, all_reduce_size)
new_flat_grads = []
for grad_group in grad_groups:
indices = reduce(lambda x, y: x + [x[-1] + sizes[y]], grad_group, [0])
big_grad = mx.concatenate(
[flat_grads[i][1].reshape(-1) for i in grad_group]
)
big_grad = (
mx.distributed.all_sum(
big_grad, stream=communication_stream, group=group
)
/ N
)
big_grad = mx.split(big_grad, indices[1:-1])
new_flat_grads.extend(
(keys[j], big_grad[i].reshape(shapes[j]))
for i, j in enumerate(grad_group)
)
return tree_unflatten(new_flat_grads)
def _clip_grads_fsdp(grads_slice, max_norm, group=None):
local_norm_sq = tree_reduce(lambda acc, g: acc + g.square().sum(), grads_slice, 0.0)
global_norm_sq = mx.distributed.all_sum(local_norm_sq, group=group)
grad_norm = mx.sqrt(global_norm_sq)
normalizer = mx.minimum(max_norm / (grad_norm + 1e-6), 1.0)
grads_slice = tree_map(lambda g: g * normalizer, grads_slice)
return grads_slice, grad_norm
def fsdp_apply_gradients(
gradients,
parameters,
optimizer,
fsdp_group=None,
dp_group=None,
communication_size=32 * 1024**2,
communication_stream=None,
max_norm=None,
):
fsdp_group = fsdp_group or mx.distributed.init()
N = fsdp_group.size() * (dp_group.size() if dp_group is not None else 1)
if N == 1:
if max_norm is not None:
gradients, grad_norm = _clip_grads_fsdp(gradients, max_norm)
return optimizer.apply_gradients(gradients, parameters), grad_norm
return optimizer.apply_gradients(gradients, parameters)
flat_grads = tree_flatten(gradients)
flat_params = tree_flatten(parameters)
keys, shapes, sizes, dtypes = _extract_info(flat_grads)
itemsize = dtypes[0].size
groups = _group_by_size(keys, sizes, itemsize, communication_size)
S = fsdp_group.size()
fsdp_rank = fsdp_group.rank()
grad_slices = {}
param_slices = {}
for group_idx, arr_group in enumerate(groups):
big_grad = mx.concatenate(
[flat_grads[i][1].reshape(S, -1) for i in arr_group], axis=1
)
grad_slices[group_idx] = (
mx.distributed.sum_scatter(
big_grad, group=fsdp_group, stream=communication_stream
)
/ N
)
if dp_group is not None:
grad_slices[group_idx] = mx.distributed.all_sum(
grad_slices[group_idx], group=dp_group, stream=communication_stream
)
big_param = mx.concatenate(
[flat_params[i][1].reshape(S, -1) for i in arr_group], axis=1
)
param_slices[group_idx] = big_param[fsdp_rank]
grad_norm = None
if max_norm is not None:
grad_slices, grad_norm = _clip_grads_fsdp(
grad_slices, max_norm, group=fsdp_group
)
updated_param_slices = optimizer.apply_gradients(grad_slices, param_slices)
new_flat = []
for group_idx, arr_group in enumerate(groups):
big_gathered = mx.distributed.all_gather(
updated_param_slices[group_idx],
group=fsdp_group,
stream=communication_stream,
)
split_sizes = [sizes[i] // S for i in arr_group]
split_indices = []
acc = 0
for s in split_sizes:
acc += s
split_indices.append(acc)
parts = mx.split(big_gathered, split_indices[:-1], axis=1)
for idx_in_group, i in enumerate(arr_group):
new_flat.append((keys[i], parts[idx_in_group].reshape(shapes[i])))
result = tree_unflatten(new_flat)
if max_norm is not None:
return result, grad_norm
return result