# Copyright (c) 2023-2026, Songlin Yang, Yu Zhang, Zhiyuan Li # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. # For a list of all contributors, visit: # https://github.com/fla-org/flash-linear-attention/graphs/contributors """ Context Parallel support for Token Shift. Token shift has a 1-token dependency on previous tokens: y[t] = x[t-1] - x[t] (for t > 0) y[0] = cache - x[0] (cache is the last token from previous rank) In CP mode, non-first ranks need the last token from the previous rank as cache. Backward: non-last ranks need to send the last token's gradient to previous rank. """ import torch import torch.distributed as dist from ..modules.token_shift import token_shift_bwd, token_shift_fwd from ..ops.cp import FLACPContext, conv_cp_send_recv_bwd, conv_cp_send_recv_fwd class TokenShiftCPFunction(torch.autograd.Function): """ Context Parallel version of TokenShift. Forward: 1. Get last token from previous rank to construct cache 2. Call token_shift_fwd with cache Backward: 1. Call token_shift_bwd to get dx 2. Sync communication: add next rank's first token gradient to current rank's last token """ @staticmethod def _prepare_cache_for_cp( x: torch.Tensor, cu_seqlens: torch.Tensor | None, context: FLACPContext, group: dist.ProcessGroup | None, ) -> tuple[torch.Tensor | None, int]: """Prepare cache for CP forward pass by communicating with previous rank. Args: x: Input tensor of shape [1, T, D] cu_seqlens: Cumulative sequence lengths context: CP context group: Process group for communication Returns: cache: Cache tensor of shape [N, D] or None pre_num_tokens: Number of tokens from previous rank for the first sequence """ if group is None: return None, 0 D = x.shape[-1] cache = None pre_num_tokens = 0 if not context.is_first_rank: # Non-first rank: need cache from previous rank assert x.dim() == 3 and x.shape[0] == 1, f"CP requires [1, T, D], got {x.shape}" x_2d = x.squeeze(0) # [T, D] last_token = x_2d[-1:].contiguous() # [1, D] prev_last_token = conv_cp_send_recv_fwd(last_token, group) # [1, D] # For varlen: only the first sequence needs cache from prev rank N = len(cu_seqlens) - 1 if cu_seqlens is not None else 1 cache = torch.zeros(N, D, device=x.device, dtype=x.dtype) # pre_num_conv_tokens tells us how many tokens from prev rank # belong to the first sequence on this rank pre_num_tokens = getattr(context, 'pre_num_conv_tokens', 0) if pre_num_tokens > 0: # The prev rank's last token is used as cache for first sequence cache[0] = prev_last_token[0] else: # First rank: participate in send but don't use received data x_2d = x.squeeze(0) last_token = x_2d[-1:].contiguous() _ = conv_cp_send_recv_fwd(last_token, group) return cache, pre_num_tokens @staticmethod def _correct_dx_for_cp( dx: torch.Tensor, grad_cache: torch.Tensor | None, group: dist.ProcessGroup | None, is_first_rank: bool, pre_num_tokens: int = 0, ) -> None: """Correct dx gradients for CP backward pass. Args: dx: Gradient tensor to be corrected, shape [1, T, D] grad_cache: Gradient w.r.t. cache, shape [N, D] or None group: Process group is_first_rank: Whether this is the first rank pre_num_tokens: Number of tokens from previous rank for first sequence """ if group is None: return D = dx.shape[-1] # Prepare gradient to send to previous rank if grad_cache is not None and pre_num_tokens > 0: # Only first sequence's cache gradient is relevant d_cache = grad_cache[0:1] # [1, D] else: d_cache = torch.zeros(1, D, device=dx.device, dtype=dx.dtype) # Send to previous rank, receive from next rank recv_grad = conv_cp_send_recv_bwd(d_cache, group) # [1, D] # Add received gradient to current rank's last token dx[0, -1, :].add_(recv_grad[0]) @staticmethod def forward( ctx, x: torch.Tensor, cu_seqlens: torch.Tensor | None, chunk_indices: torch.Tensor | None, cp_context: FLACPContext | None, ): if cp_context is None: raise ValueError("cp_context must be provided for TokenShiftCPFunction") cu_seqlens = cp_context.cu_seqlens group = cp_context.group # Prepare cache for CP cache, pre_num_tokens = TokenShiftCPFunction._prepare_cache_for_cp( x=x, cu_seqlens=cu_seqlens, context=cp_context, group=group, ) # Save for backward ctx.cu_seqlens = cu_seqlens ctx.chunk_indices = chunk_indices ctx.group = group ctx.has_cache = cache is not None ctx.is_first_rank = cp_context.is_first_rank ctx.pre_num_tokens = pre_num_tokens # Call original forward y, N, T, use_short_kernel, cache_out = token_shift_fwd( x=x, cu_seqlens=cu_seqlens, cache=cache, output_cache=True, chunk_indices=chunk_indices, ) ctx.N = N ctx.T = T ctx.use_short_kernel = use_short_kernel return y @staticmethod def backward(ctx, dy: torch.Tensor): group = ctx.group # Prepare dcache for backward # For CP: non-last rank needs to receive gradient from next rank # This is handled in _correct_dx_for_cp after computing dx dcache = None # Will be computed by token_shift_bwd # Call original backward dx, grad_cache = token_shift_bwd( dy=dy, N=ctx.N, T=ctx.T, dcache=dcache, cu_seqlens=ctx.cu_seqlens, use_short_kernel=ctx.use_short_kernel, has_init_cache=ctx.has_cache, chunk_indices=ctx.chunk_indices, ) # Correct dx gradients for CP TokenShiftCPFunction._correct_dx_for_cp( dx=dx, grad_cache=grad_cache, group=group, is_first_rank=ctx.is_first_rank, pre_num_tokens=ctx.pre_num_tokens, ) return dx, None, None, None @torch.compiler.disable def token_shift_cp( x: torch.Tensor, cp_context: FLACPContext, cu_seqlens: torch.Tensor | None = None, chunk_indices: torch.Tensor | None = None, ): """ Context Parallel version of token_shift. Args: x: Input tensor of shape [1, T, D] cp_context: CP context (required for CP mode) cu_seqlens: Cumulative sequence lengths chunk_indices: Chunk indices for variable-length sequences Returns: output: Tensor of shape [1, T, D] after applying token-shift """ if cp_context is None: raise ValueError("cp_context must be provided for token_shift_cp") assert cp_context.cu_seqlens is not None, "cu_seqlens must be provided for token_shift_cp" return TokenShiftCPFunction.apply( x, cu_seqlens, chunk_indices, cp_context )