543 lines
18 KiB
Python
543 lines
18 KiB
Python
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License
|
|
|
|
|
|
import paddle
|
|
import paddle.distributed as dist
|
|
import paddle.nn.functional as F
|
|
from paddle import _C_ops
|
|
|
|
|
|
def shard_seq_load_balance(tensor, seq_dim):
|
|
# dtensor Replicate() -> reorder -> Shard(seq_dim)
|
|
placements = tensor.placements
|
|
process_mesh = tensor.process_mesh
|
|
cp_index = process_mesh.dim_names.index('sep')
|
|
cp_degree = process_mesh.shape[cp_index]
|
|
if cp_degree > 1:
|
|
# split
|
|
sliced_datas = paddle.split(
|
|
tensor, num_or_sections=cp_degree * 2, axis=seq_dim
|
|
)
|
|
# resort [q0,q1,q2,q3] -> [q0,q3,q1,q2]
|
|
indices = []
|
|
for i in range(cp_degree):
|
|
indices.append(i)
|
|
indices.append(cp_degree * 2 - 1 - i)
|
|
reorder_indices = indices
|
|
reordered = [sliced_datas[i] for i in reorder_indices]
|
|
reordered_tensor = paddle.concat(reordered, axis=seq_dim)
|
|
# reshard q/k/v -> Shard(seq_dim)
|
|
placements[cp_index] = paddle.distributed.Shard(seq_dim) # seq_dim:1
|
|
tensor = paddle.distributed.reshard(
|
|
reordered_tensor, process_mesh, placements
|
|
)
|
|
return tensor
|
|
|
|
|
|
def unshard_seq_load_balance(tensor, seq_dim):
|
|
# dtensor Shard(seq_dim) -> Replicate() -> reorder
|
|
placements = tensor.placements
|
|
process_mesh = tensor.process_mesh
|
|
cp_index = process_mesh.dim_names.index('sep')
|
|
cp_degree = process_mesh.shape[cp_index]
|
|
all_tensor = dist.reshard(tensor, process_mesh, [dist.Replicate()])
|
|
sliced_datas = paddle.split(
|
|
all_tensor, num_or_sections=cp_degree * 2, axis=seq_dim
|
|
)
|
|
reorder_indices = []
|
|
for i in range(cp_degree):
|
|
reorder_indices.append(i)
|
|
reorder_indices.append(cp_degree * 2 - 1 - i)
|
|
inverse_indices = [0] * len(reorder_indices)
|
|
for idx, v in enumerate(reorder_indices):
|
|
inverse_indices[v] = idx
|
|
restored = [sliced_datas[i] for i in inverse_indices]
|
|
return paddle.concat(restored, axis=seq_dim)
|
|
|
|
|
|
class RingCommunicator:
|
|
def __init__(self, group, local_key, local_value):
|
|
self._k_buffer = [
|
|
local_key.clone().contiguous(),
|
|
local_key.clone().contiguous(),
|
|
]
|
|
self._v_buffer = [
|
|
local_value.clone().contiguous(),
|
|
local_value.clone().contiguous(),
|
|
]
|
|
|
|
self._next_buffer_idx = 0
|
|
|
|
self.group = group
|
|
mesh = dist.auto_parallel.get_mesh()
|
|
process_id = dist.get_rank()
|
|
self.group_rank = mesh.get_rank_by_dim_and_process_id("sep", process_id)
|
|
self.cp_size = mesh.get_dim_size("sep")
|
|
cp_index = mesh.dim_names.index("sep")
|
|
|
|
self.send_rank = self.group.ranks[
|
|
(self.group_rank + 1) % self.cp_size
|
|
] # 1%2=1
|
|
self.recv_rank = self.group.ranks[(self.group_rank - 1) % self.cp_size]
|
|
|
|
self._reqs = []
|
|
|
|
def wait(self):
|
|
paddle.device.synchronize()
|
|
|
|
def add_to_buffers(self, key, value):
|
|
if key.shape != self._k_buffer[self._next_buffer_idx].shape:
|
|
self._k_buffer[self._next_buffer_idx][:, : key.shape[1], :, :].add_(
|
|
key
|
|
)
|
|
self._v_buffer[self._next_buffer_idx][:, : key.shape[1], :, :].add_(
|
|
value
|
|
)
|
|
else:
|
|
self._k_buffer[self._next_buffer_idx].add_(key)
|
|
self._v_buffer[self._next_buffer_idx].add_(value)
|
|
|
|
def get_buffers(self):
|
|
return (
|
|
self._k_buffer[self._next_buffer_idx],
|
|
self._v_buffer[self._next_buffer_idx],
|
|
)
|
|
|
|
def send_recv(self):
|
|
send_k_op = dist.P2POp(
|
|
dist.isend,
|
|
self._k_buffer[self._next_buffer_idx].contiguous(),
|
|
self.send_rank,
|
|
self.group,
|
|
)
|
|
send_v_op = dist.P2POp(
|
|
dist.isend,
|
|
self._v_buffer[self._next_buffer_idx].contiguous(),
|
|
self.send_rank,
|
|
self.group,
|
|
)
|
|
recv_k_op = dist.P2POp(
|
|
dist.irecv,
|
|
self._k_buffer[(self._next_buffer_idx + 1) % 2],
|
|
self.recv_rank,
|
|
self.group,
|
|
)
|
|
recv_v_op = dist.P2POp(
|
|
dist.irecv,
|
|
self._v_buffer[(self._next_buffer_idx + 1) % 2],
|
|
self.recv_rank,
|
|
self.group,
|
|
)
|
|
|
|
self._next_buffer_idx = (self._next_buffer_idx + 1) % 2
|
|
|
|
ops = [send_k_op, send_v_op, recv_k_op, recv_v_op]
|
|
|
|
self._reqs = dist.batch_isend_irecv(ops)
|
|
|
|
|
|
def update_out_and_lse(
|
|
old_out, old_lse, block_out, block_lse, second_chunk_only=False
|
|
):
|
|
if second_chunk_only:
|
|
second_chunk_out = old_out[:, old_out.shape[1] // 2 :, :, :]
|
|
second_chunk_lse = old_lse[:, old_lse.shape[1] // 2 :, :, :]
|
|
second_chunk_out, second_chunk_lse = update_out_and_lse(
|
|
second_chunk_out, second_chunk_lse, block_out, block_lse
|
|
)
|
|
old_out[:, old_out.shape[1] // 2 :, :, :] = second_chunk_out
|
|
old_lse[:, old_lse.shape[1] // 2 :, :, :] = second_chunk_lse
|
|
return old_out, old_lse
|
|
else:
|
|
block_out, block_lse = (
|
|
paddle.cast(block_out, "float32"),
|
|
paddle.cast(block_lse, "float32"),
|
|
)
|
|
with paddle.amp.auto_cast(enable=False):
|
|
return old_out - (old_out - block_out) * F.sigmoid(
|
|
block_lse - old_lse
|
|
), old_lse - F.log_sigmoid(old_lse - block_lse)
|
|
|
|
|
|
def get_chunk_id(rank, cp_size):
|
|
return rank, (2 * cp_size - 1 - rank)
|
|
|
|
|
|
def concat_masks(attn_masks_list, rank, cp_size):
|
|
assert len(attn_masks_list) == 2 * cp_size
|
|
first_chunk_id, second_chunk_id = get_chunk_id(rank, cp_size)
|
|
return paddle.concat(
|
|
[attn_masks_list[first_chunk_id], attn_masks_list[second_chunk_id]],
|
|
axis=3,
|
|
)
|
|
|
|
|
|
def ring_flash_attention_forward_func(
|
|
group,
|
|
local_query,
|
|
local_key,
|
|
local_value,
|
|
attn_mask=None,
|
|
dropout=0.0,
|
|
is_causal=False,
|
|
fixed_seed_offset=None,
|
|
training=True,
|
|
):
|
|
cp_size = group.world_size
|
|
group_rank = group.rank
|
|
|
|
comm_buffer = RingCommunicator(group, local_key, local_value)
|
|
local_q_seq_len = local_query.shape[1]
|
|
if attn_mask is not None:
|
|
attn_masks_list = paddle.split(
|
|
attn_mask, num_or_sections=cp_size * 2, axis=3
|
|
)
|
|
if is_causal:
|
|
local_query_second_chunk = local_query[
|
|
:, local_q_seq_len // 2 :, :, :
|
|
].contiguous()
|
|
for step in range(cp_size):
|
|
block_k, block_v = comm_buffer.get_buffers()
|
|
if step != cp_size - 1:
|
|
comm_buffer.send_recv()
|
|
if not is_causal:
|
|
# out [bs, seq, nhead, headdim]
|
|
# lse [bs, nhead, seq]
|
|
block_out, _, block_lse, _ = _C_ops.flash_attn(
|
|
local_query,
|
|
block_k,
|
|
block_v,
|
|
fixed_seed_offset,
|
|
(
|
|
None
|
|
if attn_mask is None
|
|
else concat_masks(
|
|
attn_masks_list, (group_rank - step) % cp_size, cp_size
|
|
)
|
|
),
|
|
dropout,
|
|
False,
|
|
False,
|
|
not training,
|
|
"",
|
|
)
|
|
paddle.unsqueeze_(paddle.transpose_(block_lse, [0, 2, 1]), axis=-1)
|
|
|
|
if step == 0:
|
|
out, lse = block_out, block_lse
|
|
else:
|
|
out, lse = update_out_and_lse(out, lse, block_out, block_lse)
|
|
else:
|
|
if step == 0:
|
|
block_out, _, block_lse, _ = _C_ops.flash_attn(
|
|
local_query,
|
|
block_k,
|
|
block_v,
|
|
fixed_seed_offset,
|
|
None,
|
|
dropout,
|
|
True,
|
|
False,
|
|
not training,
|
|
"",
|
|
)
|
|
paddle.unsqueeze_(
|
|
paddle.transpose_(block_lse, [0, 2, 1]), axis=-1
|
|
)
|
|
out, lse = block_out, block_lse
|
|
elif step > group_rank:
|
|
block_out, _, block_lse, _ = _C_ops.flash_attn(
|
|
local_query_second_chunk,
|
|
block_k,
|
|
block_v,
|
|
fixed_seed_offset,
|
|
None,
|
|
dropout,
|
|
False,
|
|
False,
|
|
not training,
|
|
"",
|
|
)
|
|
block_lse = block_lse[:, :, 0 : (local_q_seq_len // 2)]
|
|
paddle.unsqueeze_(
|
|
paddle.transpose_(block_lse, [0, 2, 1]), axis=-1
|
|
)
|
|
out, lse = update_out_and_lse(
|
|
out, lse, block_out, block_lse, True
|
|
)
|
|
else:
|
|
block_out, _, block_lse, _ = _C_ops.flash_attn(
|
|
local_query,
|
|
block_k[:, : local_q_seq_len // 2, :, :],
|
|
block_v[:, : local_q_seq_len // 2, :, :],
|
|
fixed_seed_offset,
|
|
None,
|
|
dropout,
|
|
False,
|
|
False,
|
|
not training,
|
|
"",
|
|
)
|
|
paddle.unsqueeze_(
|
|
paddle.transpose_(block_lse, [0, 2, 1]), axis=-1
|
|
)
|
|
out, lse = update_out_and_lse(out, lse, block_out, block_lse)
|
|
|
|
paddle.device.synchronize()
|
|
out = paddle.cast(out, local_query.dtype)
|
|
lse = paddle.transpose_(paddle.squeeze(lse, axis=-1), [0, 2, 1])
|
|
return out, lse
|
|
|
|
|
|
def ring_flash_attention_backward_func(
|
|
group,
|
|
local_out_grad,
|
|
local_query,
|
|
local_key,
|
|
local_value,
|
|
local_out,
|
|
lse,
|
|
attn_mask,
|
|
dropout=0.0,
|
|
is_causal=False,
|
|
fixed_seed_offset=None,
|
|
):
|
|
cp_size = group.world_size
|
|
group_rank = group.rank
|
|
|
|
lse = lse.contiguous()
|
|
|
|
local_q_seq_len = local_query.shape[1]
|
|
query_grad_buffer = paddle.zeros_like(local_query)
|
|
key_grad_buffer = paddle.zeros_like(local_key)
|
|
value_grad_buffer = paddle.zeros_like(local_value)
|
|
|
|
kv_comm_buffer = RingCommunicator(group, local_key, local_value)
|
|
grad_comm_buffer = RingCommunicator(
|
|
group, key_grad_buffer, value_grad_buffer
|
|
)
|
|
if is_causal:
|
|
local_query_second_chunk = local_query[:, local_q_seq_len // 2 :, :, :]
|
|
local_out_second_chunk = local_out[:, local_q_seq_len // 2 :, :, :]
|
|
lse_second_chunk = lse[:, :, local_q_seq_len // 2 :].contiguous()
|
|
out_grad_second_chunk = local_out_grad[:, local_q_seq_len // 2 :, :, :]
|
|
|
|
if attn_mask is not None:
|
|
attn_masks_list = paddle.split(
|
|
attn_mask, num_or_sections=cp_size * 2, axis=3
|
|
)
|
|
for step in range(cp_size):
|
|
block_k, block_v = kv_comm_buffer.get_buffers()
|
|
if step != cp_size - 1:
|
|
kv_comm_buffer.send_recv()
|
|
|
|
if not is_causal:
|
|
block_q_grad, block_k_grad, block_v_grad = _C_ops.flash_attn_grad(
|
|
local_query,
|
|
block_k,
|
|
block_v,
|
|
local_out,
|
|
lse,
|
|
fixed_seed_offset,
|
|
(
|
|
None
|
|
if attn_mask is None
|
|
else concat_masks(
|
|
attn_masks_list, (group_rank - step) % cp_size, cp_size
|
|
)
|
|
),
|
|
local_out_grad,
|
|
dropout,
|
|
False,
|
|
)
|
|
query_grad_buffer.add_(block_q_grad)
|
|
else:
|
|
if step == 0:
|
|
block_q_grad, block_k_grad, block_v_grad = (
|
|
_C_ops.flash_attn_grad(
|
|
local_query,
|
|
block_k,
|
|
block_v,
|
|
local_out,
|
|
lse,
|
|
fixed_seed_offset,
|
|
None,
|
|
local_out_grad,
|
|
dropout,
|
|
True,
|
|
)
|
|
)
|
|
query_grad_buffer.add_(block_q_grad)
|
|
elif step > group_rank:
|
|
block_q_grad, block_k_grad, block_v_grad = (
|
|
_C_ops.flash_attn_grad(
|
|
local_query_second_chunk,
|
|
block_k,
|
|
block_v,
|
|
local_out_second_chunk,
|
|
lse_second_chunk,
|
|
fixed_seed_offset,
|
|
None,
|
|
out_grad_second_chunk,
|
|
dropout,
|
|
False,
|
|
)
|
|
)
|
|
query_grad_buffer[:, local_q_seq_len // 2 :, :, :].add_(
|
|
block_q_grad
|
|
)
|
|
else:
|
|
block_q_grad, block_k_grad, block_v_grad = (
|
|
_C_ops.flash_attn_grad(
|
|
local_query,
|
|
block_k[:, : local_q_seq_len // 2, :, :],
|
|
block_v[:, : local_q_seq_len // 2, :, :],
|
|
local_out,
|
|
lse,
|
|
fixed_seed_offset,
|
|
None,
|
|
local_out_grad,
|
|
dropout,
|
|
False,
|
|
)
|
|
)
|
|
query_grad_buffer.add_(block_q_grad)
|
|
paddle.device.synchronize()
|
|
|
|
grad_comm_buffer.add_to_buffers(
|
|
block_k_grad.contiguous(), block_v_grad.contiguous()
|
|
)
|
|
grad_comm_buffer.send_recv()
|
|
|
|
grad_comm_buffer.wait()
|
|
key_grad_buffer, value_grad_buffer = grad_comm_buffer.get_buffers()
|
|
|
|
return query_grad_buffer, key_grad_buffer, value_grad_buffer
|
|
|
|
|
|
class RingFlashAttention(paddle.autograd.PyLayer):
|
|
@staticmethod
|
|
def forward(
|
|
ctx,
|
|
query,
|
|
key,
|
|
value,
|
|
attn_mask=None,
|
|
dropout=0.0,
|
|
is_causal=False,
|
|
fixed_seed_offset=None,
|
|
training=True,
|
|
):
|
|
if dropout > 0.0:
|
|
raise NotImplementedError(
|
|
"Dropout is not supported in ring attention yet."
|
|
)
|
|
mesh = dist.auto_parallel.get_mesh()
|
|
cp_index = mesh.dim_names.index('sep')
|
|
process_id = dist.get_rank()
|
|
rank = mesh.get_rank_by_dim_and_process_id("sep", process_id)
|
|
dist.init_parallel_env()
|
|
|
|
group = mesh._get_group("sep")
|
|
local_query = dist.auto_parallel.api.dtensor_to_local(
|
|
query, query.process_mesh, query.placements
|
|
)
|
|
local_key = dist.auto_parallel.api.dtensor_to_local(
|
|
key, key.process_mesh, key.placements
|
|
)
|
|
local_value = dist.auto_parallel.api.dtensor_to_local(
|
|
value, value.process_mesh, value.placements
|
|
)
|
|
if attn_mask is not None:
|
|
is_causal = False
|
|
|
|
out, lse = ring_flash_attention_forward_func(
|
|
group,
|
|
local_query,
|
|
local_key,
|
|
local_value,
|
|
attn_mask,
|
|
dropout,
|
|
is_causal,
|
|
fixed_seed_offset,
|
|
training,
|
|
)
|
|
ctx.save_for_backward(group, query, key, value, out, lse, attn_mask)
|
|
ctx.fixed_seed_offset = fixed_seed_offset
|
|
ctx.dropout = dropout
|
|
ctx.is_causal = is_causal
|
|
out_dtensor = dist.auto_parallel.api.dtensor_from_local(
|
|
out, query.process_mesh, query.placements
|
|
)
|
|
return out_dtensor.contiguous()
|
|
|
|
@staticmethod
|
|
def backward(ctx, out_grad):
|
|
mesh = dist.auto_parallel.get_mesh()
|
|
cp_index = mesh.dim_names.index('sep')
|
|
group, query, key, value, out, lse, attn_mask = ctx.saved_tensor()
|
|
fixed_seed_offset = ctx.fixed_seed_offset
|
|
dropout = ctx.dropout
|
|
is_causal = ctx.is_causal
|
|
|
|
if fixed_seed_offset is None:
|
|
fixed_seed_offset = paddle.to_tensor(
|
|
[0, 0], place=paddle.CPUPlace(), dtype=paddle.int64
|
|
)
|
|
local_query = dist.auto_parallel.api.dtensor_to_local(
|
|
query, query.process_mesh, query.placements
|
|
)
|
|
local_key = dist.auto_parallel.api.dtensor_to_local(
|
|
key, key.process_mesh, key.placements
|
|
)
|
|
local_value = dist.auto_parallel.api.dtensor_to_local(
|
|
value, value.process_mesh, value.placements
|
|
)
|
|
local_out_grad = dist.auto_parallel.api.dtensor_to_local(
|
|
out_grad, out_grad.process_mesh, out_grad.placements
|
|
)
|
|
query_grad, key_grad, value_grad = ring_flash_attention_backward_func(
|
|
group,
|
|
local_out_grad,
|
|
local_query,
|
|
local_key,
|
|
local_value,
|
|
out,
|
|
lse,
|
|
attn_mask,
|
|
dropout,
|
|
is_causal,
|
|
fixed_seed_offset,
|
|
)
|
|
query_grad_dtensor = dist.auto_parallel.api.dtensor_from_local(
|
|
query_grad, query.process_mesh, query.placements
|
|
)
|
|
key_grad_dtensor = dist.auto_parallel.api.dtensor_from_local(
|
|
key_grad, key.process_mesh, key.placements
|
|
)
|
|
value_grad_dtensor = dist.auto_parallel.api.dtensor_from_local(
|
|
value_grad, value.process_mesh, value.placements
|
|
)
|
|
|
|
if attn_mask is not None and not attn_mask.stop_gradient:
|
|
return (
|
|
query_grad_dtensor,
|
|
key_grad_dtensor,
|
|
value_grad_dtensor,
|
|
None,
|
|
)
|
|
else:
|
|
return query_grad_dtensor, key_grad_dtensor, value_grad_dtensor
|