| |
| |
| |
| |
|
|
| from typing import Callable, Optional, Tuple |
|
|
| import torch |
|
|
|
|
| def _cycle_over_all_chunks( |
| my_chunk: torch.Tensor, |
| pg: torch.distributed.ProcessGroup, |
| step_fn: Callable[ |
| [torch.Tensor, int, Optional[torch.distributed.Work], Optional[torch.Tensor]], |
| Optional[torch.Tensor], |
| ], |
| ): |
| next_rank = (pg.rank() + 1) % pg.size() |
| prev_rank = (pg.rank() - 1) % pg.size() |
|
|
| extra_req: Optional[torch.distributed.Work] = None |
| dst_extra_chunk: Optional[torch.Tensor] = None |
|
|
| dst_chunk = torch.empty_like(my_chunk) |
| for iter_ in range(pg.size()): |
| src_chunk = my_chunk if iter_ == 0 else dst_chunk |
| dst_chunk = torch.empty_like(my_chunk) |
|
|
| if iter_ < pg.size() - 1: |
| send_op = torch.distributed.P2POp( |
| torch.distributed.isend, src_chunk, next_rank, group=pg |
| ) |
| recv_op = torch.distributed.P2POp( |
| torch.distributed.irecv, dst_chunk, prev_rank, group=pg |
| ) |
| reqs = torch.distributed.batch_isend_irecv([send_op, recv_op]) |
| else: |
| reqs = [] |
|
|
| src_extra_chunk = step_fn( |
| src_chunk, (pg.rank() - iter_) % pg.size(), extra_req, dst_extra_chunk |
| ) |
| if src_extra_chunk is not None: |
| dst_extra_chunk = torch.empty_like(src_extra_chunk) |
| send_op = torch.distributed.P2POp( |
| torch.distributed.isend, src_extra_chunk, next_rank, group=pg |
| ) |
| recv_op = torch.distributed.P2POp( |
| torch.distributed.irecv, dst_extra_chunk, prev_rank, group=pg |
| ) |
| (extra_req,) = torch.distributed.batch_isend_irecv([send_op, recv_op]) |
| else: |
| extra_req = None |
| dst_extra_chunk = None |
|
|
| for req in reqs: |
| req.wait() |
|
|
| return extra_req, dst_extra_chunk |
|
|
|
|
| class MemoryEfficientClipLoss(torch.autograd.Function): |
| @staticmethod |
| def forward( |
| ctx: torch.autograd.function.FunctionCtx, |
| pg: torch.distributed.ProcessGroup, |
| image_features: torch.Tensor, |
| text_features: torch.Tensor, |
| logit_scale: torch.Tensor, |
| ) -> torch.Tensor: |
| image_partial_lses_for_me = torch.empty( |
| (pg.size(), image_features.shape[0]), |
| dtype=torch.float32, |
| device=image_features.device, |
| ) |
| text_partial_lses_for_others = torch.empty( |
| (pg.size(), text_features.shape[0]), |
| dtype=torch.float32, |
| device=text_features.device, |
| ) |
|
|
| positives: Optional[torch.Tensor] = None |
|
|
| def my_step( |
| incoming: torch.Tensor, |
| other_rank: int, |
| _req: Optional[torch.distributed.Work], |
| _extra: Optional[torch.Tensor], |
| ) -> None: |
| nonlocal positives |
| logits = logit_scale * (image_features @ incoming.T) |
| if other_rank == pg.rank(): |
| positives = torch.diag(logits) |
| torch.logsumexp(logits, dim=1, out=image_partial_lses_for_me[other_rank]) |
| torch.logsumexp(logits, dim=0, out=text_partial_lses_for_others[other_rank]) |
|
|
| _cycle_over_all_chunks(text_features, pg, my_step) |
|
|
| text_partial_lses_for_me = torch.empty_like(text_partial_lses_for_others) |
| torch.distributed.all_to_all_single( |
| text_partial_lses_for_me, text_partial_lses_for_others, group=pg |
| ) |
|
|
| image_lses_for_me = torch.logsumexp(image_partial_lses_for_me, dim=0) |
| text_lses_for_me = torch.logsumexp(text_partial_lses_for_me, dim=0) |
|
|
| assert positives is not None |
| ctx.save_for_backward( |
| image_features, |
| text_features, |
| logit_scale, |
| positives, |
| image_lses_for_me, |
| text_lses_for_me, |
| ) |
| ctx.pg = pg |
|
|
| return (-(2 * positives - image_lses_for_me - text_lses_for_me).mean() / 2).to( |
| positives.dtype |
| ) |
|
|
| @staticmethod |
| def backward( |
| ctx: torch.autograd.function.FunctionCtx, *grad_outputs: torch.Tensor |
| ) -> Tuple[Optional[torch.Tensor], ...]: |
| pg: torch.distributed.ProcessGroup = ctx.pg |
| image_features: torch.Tensor |
| text_features: torch.Tensor |
| logit_scale: torch.Tensor |
| positives: torch.Tensor |
| image_lses_for_me: torch.Tensor |
| text_lses_for_me: torch.Tensor |
| ( |
| image_features, |
| text_features, |
| logit_scale, |
| positives, |
| image_lses_for_me, |
| text_lses_for_me, |
| ) = ctx.saved_tensors |
|
|
| (grad,) = grad_outputs |
| grad /= 2 * positives.numel() |
|
|
| text_lse_for_others = text_lses_for_me.new_empty( |
| (pg.size(),) + text_lses_for_me.shape |
| ) |
| torch.distributed.all_gather_into_tensor( |
| text_lse_for_others, text_lses_for_me, group=pg |
| ) |
|
|
| grad_image_features = torch.zeros_like(image_features) |
| grad_logit_scale = torch.zeros_like(logit_scale) |
|
|
| def my_step( |
| incoming: torch.Tensor, |
| other_rank: int, |
| req: Optional[torch.distributed.Work], |
| grad_text_features: Optional[torch.Tensor], |
| ) -> torch.Tensor: |
| raw_logits = image_features @ incoming.T |
| logits = logit_scale * raw_logits |
|
|
| grad_logits = ( |
| (logits - image_lses_for_me[:, None]).exp() |
| + (logits - text_lse_for_others[other_rank, None, :]).exp() |
| ).to(logits.dtype) |
| if other_rank == pg.rank(): |
| torch.diagonal(grad_logits).sub_(2) |
|
|
| grad_logit_scale.add_((raw_logits * grad_logits).sum()) |
| grad_raw_logits = grad_logits * logit_scale |
|
|
| grad_image_features.addmm_(grad_raw_logits, incoming) |
| if req is None: |
| grad_text_features = torch.matmul(grad_raw_logits.T, image_features) |
| else: |
| req.wait() |
| assert grad_text_features is not None |
| grad_text_features.addmm_(grad_raw_logits.T, image_features) |
|
|
| return grad_text_features |
|
|
| req, grad_text_features = _cycle_over_all_chunks(text_features, pg, my_step) |
| req.wait() |
|
|
| return ( |
| None, |
| grad * grad_image_features, |
| grad * grad_text_features, |
| grad * grad_logit_scale, |
| ) |
|
|
|
|
| def memory_efficient_clip_loss( |
| image_features: torch.Tensor, |
| text_features: torch.Tensor, |
| logit_scale: torch.Tensor, |
| *, |
| group: torch.distributed.ProcessGroup, |
| ) -> torch.Tensor: |
| return MemoryEfficientClipLoss.apply( |
| group, image_features.float(), text_features.float(), logit_scale.float() |
| ) |
|
|