Files
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

460 lines
14 KiB
Python

# Some code borrowed from the awesome work: https://github.com/zhuzilin/ring-flash-attention
# Copyright (c) ModelScope Contributors. All rights reserved.
import torch
from functools import cache
from .ring_utils import RingComm
_NPU_BLOCK_MASK_SIZE = 2048
_NPU_FULL_TOKENS = 2147483647
_NPU_TND_SOFTMAX_STAT_REPEAT = 8
def is_npu_tensor(tensor: torch.Tensor) -> bool:
return tensor.device.type == 'npu'
def _cu_seqlens_to_actual_seq(cu_seqlens: torch.Tensor) -> tuple[int, ...]:
return tuple(int(x) for x in cu_seqlens[1:].detach().cpu().tolist())
@cache
def _get_npu_causal_mask_cpu() -> torch.Tensor:
return torch.triu(torch.ones((_NPU_BLOCK_MASK_SIZE, _NPU_BLOCK_MASK_SIZE), dtype=torch.bool), diagonal=1)
def _get_npu_causal_mask(device: torch.device) -> torch.Tensor:
return _get_npu_causal_mask_cpu().to(device=device)
def _normalize_window_size(window_size):
if window_size is None:
return -1, -1
return window_size
def _get_npu_sparse_params(causal: bool, window_size, device: torch.device) -> dict:
window_size = _normalize_window_size(window_size)
if window_size != (-1, -1):
left, right = window_size
left = _NPU_FULL_TOKENS if left < 0 else int(left)
right = _NPU_FULL_TOKENS if right < 0 else int(right)
if causal:
right = 0
return {
'atten_mask': _get_npu_causal_mask(device),
'sparse_mode': 4,
'pre_tockens': left,
'next_tockens': right,
}
if causal:
return {
'atten_mask': _get_npu_causal_mask(device),
'sparse_mode': 3,
'pre_tockens': _NPU_FULL_TOKENS,
'next_tockens': _NPU_FULL_TOKENS,
}
return {
'atten_mask': None,
'sparse_mode': 0,
'pre_tockens': _NPU_FULL_TOKENS,
'next_tockens': _NPU_FULL_TOKENS,
}
def _reshape_npu_lse(lse: torch.Tensor, seqlen_q: int, num_heads: int) -> torch.Tensor:
"""Normalize Ascend softmax stats to flash-attn's [num_heads, seqlen] layout."""
if lse.dim() == 2:
if lse.shape == (num_heads, seqlen_q):
return lse.contiguous()
if lse.shape == (seqlen_q, num_heads):
return lse.transpose(0, 1).contiguous()
elif lse.dim() == 3:
# Some CANN versions return an extra trailing size-8 axis with repeated
# stats. Ring merge only needs one copy of each token/head lse.
if lse.shape[-1] == 8:
lse = lse[..., 0]
if lse.shape == (seqlen_q, num_heads):
return lse.transpose(0, 1).contiguous()
if lse.shape == (num_heads, seqlen_q):
return lse.contiguous()
if lse.shape[0] == seqlen_q:
return lse.permute(1, 2, 0).reshape(num_heads, seqlen_q).contiguous()
if lse.shape[1] == seqlen_q:
return lse.permute(0, 2, 1).reshape(num_heads, seqlen_q).contiguous()
raise RuntimeError(f'Unexpected NPU lse shape {tuple(lse.shape)} for seqlen_q={seqlen_q}, num_heads={num_heads}')
def _get_npu_attention_common_kwargs(
q: torch.Tensor,
*,
cu_seqlens_q: torch.Tensor,
cu_seqlens_kv: torch.Tensor,
softmax_scale: float,
dropout_p: float,
causal: bool,
window_size,
deterministic: bool,
) -> dict:
sparse_params = _get_npu_sparse_params(causal, window_size, q.device)
return {
'head_num': q.shape[1],
'input_layout': 'TND',
'scale_value': softmax_scale or q.shape[-1]**(-0.5),
'keep_prob': 1. - dropout_p,
'actual_seq_qlen': _cu_seqlens_to_actual_seq(cu_seqlens_q),
'actual_seq_kvlen': _cu_seqlens_to_actual_seq(cu_seqlens_kv),
'sync': bool(deterministic and dropout_p > 0),
**sparse_params,
}
def _call_npu_fusion_attention(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
cu_seqlens_q: torch.Tensor,
cu_seqlens_kv: torch.Tensor,
softmax_scale: float,
dropout_p: float,
causal: bool,
window_size,
deterministic: bool,
):
import torch_npu
common_kwargs = _get_npu_attention_common_kwargs(
q,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_kv=cu_seqlens_kv,
softmax_scale=softmax_scale,
dropout_p=dropout_p,
causal=causal,
window_size=window_size,
deterministic=deterministic,
)
params = {
'query': q,
'key': k,
'value': v,
'scale': common_kwargs['scale_value'],
'softmax_layout': 'TND',
}
params.update(common_kwargs)
params.pop('scale_value')
return torch_npu.npu_fusion_attention(**params)
def _call_npu_fusion_attention_grad(
dout: torch.Tensor,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
*,
attention_out: torch.Tensor,
softmax_lse: torch.Tensor,
cu_seqlens_q: torch.Tensor,
cu_seqlens_kv: torch.Tensor,
softmax_scale: float,
dropout_p: float,
causal: bool,
window_size,
deterministic: bool,
):
import torch_npu
if not hasattr(torch_npu, 'npu_fusion_attention_grad'):
raise AttributeError('torch_npu.npu_fusion_attention_grad is not available')
# Dropout backward needs the exact seed/offset from the original forward,
# which this ring ctx does not save. Fail instead of using a wrong mask.
if dropout_p != 0.0:
raise NotImplementedError('NPU ring attention native backward currently requires dropout_p=0.')
common_kwargs = _get_npu_attention_common_kwargs(
q,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_kv=cu_seqlens_kv,
softmax_scale=softmax_scale,
dropout_p=dropout_p,
causal=causal,
window_size=window_size,
deterministic=deterministic,
)
softmax_max, softmax_sum = _npu_softmax_stats_from_global_lse(
softmax_lse,
q_tokens=q.shape[0],
num_heads=q.shape[1],
)
params = {
'query': q,
'key': k,
'value': v,
'dy': dout,
'head_num': common_kwargs['head_num'],
'input_layout': common_kwargs['input_layout'],
'atten_mask': common_kwargs['atten_mask'],
'softmax_max': softmax_max,
'softmax_sum': softmax_sum,
'softmax_in': None,
'attention_in': (attention_out if torch.is_tensor(attention_out) and attention_out.numel() > 0 else None),
'scale_value': common_kwargs['scale_value'],
'keep_prob': common_kwargs['keep_prob'],
'pre_tockens': common_kwargs['pre_tockens'],
'next_tockens': common_kwargs['next_tockens'],
'seed': 0,
'offset': 0,
'numels': 0,
'actual_seq_qlen': common_kwargs['actual_seq_qlen'],
'actual_seq_kvlen': common_kwargs['actual_seq_kvlen'],
'sparse_mode': common_kwargs['sparse_mode'],
'sync': common_kwargs['sync'],
'softmax_layout': 'TND',
}
return torch_npu.npu_fusion_attention_grad(**params)
def _normalize_flash_attn_lse(softmax_lse: torch.Tensor, total_len: int) -> torch.Tensor:
"""Normalize flash-attn lse to [num_heads, total_len]."""
lse = softmax_lse
if lse.dim() == 3 and lse.shape[0] == 1:
lse = lse.squeeze(0)
if lse.dim() != 2:
raise RuntimeError(f'Unexpected softmax_lse shape: {tuple(softmax_lse.shape)}')
if lse.shape[1] != total_len:
lse = lse.transpose(0, 1).contiguous()
if lse.shape[1] != total_len:
raise RuntimeError(f'Unexpected softmax_lse shape: {tuple(softmax_lse.shape)} for total_len={total_len}')
return lse
def _npu_softmax_stats_from_global_lse(
softmax_lse_global: torch.Tensor,
q_tokens: int,
num_heads: int,
) -> tuple[torch.Tensor, torch.Tensor]:
lse_h_t = _normalize_flash_attn_lse(softmax_lse_global, q_tokens)
if lse_h_t.shape[0] != num_heads:
raise RuntimeError(f'Unexpected global lse shape: {tuple(softmax_lse_global.shape)} '
f'for q_tokens={q_tokens}, num_heads={num_heads}')
# With softmax_layout='TND', Ascend returns softmax stats as [T, N, 8].
# The split-attention backward only needs logsumexp; max=lse and sum=1
# encode the same value without replaying the block forward.
lse_t_h = lse_h_t.transpose(0, 1).contiguous().to(torch.float32)
softmax_max = lse_t_h.unsqueeze(-1).expand(
q_tokens,
num_heads,
_NPU_TND_SOFTMAX_STAT_REPEAT,
).contiguous()
return softmax_max, torch.ones_like(softmax_max)
def _get_second_half_lse(softmax_lse: torch.Tensor, cu_seqlens: torch.Tensor) -> torch.Tensor:
total_len = int(cu_seqlens[-1].item())
lse = _normalize_flash_attn_lse(softmax_lse, total_len)
# The step > rank branch only differentiates q[half_index1]. Slice the final
# merged lse per sequence so the native grad ctx sees the same query span.
second_half_lse = torch.empty((lse.shape[0], lse.shape[1] // 2), dtype=lse.dtype, device=lse.device)
for i in range(len(cu_seqlens) - 1):
start, end = cu_seqlens[i].item(), cu_seqlens[i + 1].item()
new_start, new_end = start // 2, end // 2
start += (end - start) // 2
second_half_lse[:, new_start:new_end] = lse[:, start:end]
return second_half_lse
def _npu_block_backward_with_global_stats(
block_dout,
block_q,
block_k,
block_v,
block_out_global,
block_lse_global,
block_causal,
cu_seqlens_q,
cu_seqlens_kv,
softmax_scale,
dropout_p,
window_size,
deterministic,
):
"""Run one native NPU block backward using the final merged ring stats."""
return _call_npu_fusion_attention_grad(
block_dout.to(block_q.dtype),
block_q,
block_k,
block_v,
attention_out=block_out_global,
softmax_lse=block_lse_global,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_kv=cu_seqlens_kv,
softmax_scale=softmax_scale,
dropout_p=dropout_p,
causal=block_causal,
window_size=window_size,
deterministic=deterministic,
)[:3]
def _squeeze_batch(*tensors):
squeezed = []
for tensor in tensors:
if tensor.shape[0] == 1:
squeezed.append(tensor.squeeze(0))
else:
squeezed.append(tensor)
return tuple(squeezed)
def npu_backward(
process_group,
dout,
q,
k,
v,
out,
softmax_lse,
cu_seqlens,
max_seqlen,
half_index0,
half_index1,
softmax_scale,
dropout_p=0.0,
window_size=(-1, -1),
deterministic=False,
):
kv_comm = RingComm(process_group)
d_kv_comm = RingComm(process_group)
dout, q, k, v, out, softmax_lse = _squeeze_batch(dout, q, k, v, out, softmax_lse)
cu_seqlens = cu_seqlens // kv_comm.world_size
del max_seqlen
half_cu_seqlens = cu_seqlens // 2
q1 = q[half_index1]
dout1 = dout[half_index1]
out1 = out[half_index1]
softmax_lse1 = _get_second_half_lse(softmax_lse, cu_seqlens)
dq = torch.zeros_like(q, dtype=torch.float32)
current_step_dk = torch.empty_like(k, dtype=torch.float32)
current_step_dv = torch.empty_like(v, dtype=torch.float32)
next_dk = next_dv = None
for step in range(kv_comm.world_size):
current_step_dk.zero_()
current_step_dv.zero_()
if step == 0:
bdq, bdk, bdv = _npu_block_backward_with_global_stats(
dout,
q,
k,
v,
out,
softmax_lse,
True,
cu_seqlens,
cu_seqlens,
softmax_scale,
dropout_p,
window_size,
deterministic,
)
dq += bdq.to(torch.float32)
current_step_dk += bdk.to(torch.float32)
current_step_dv += bdv.to(torch.float32)
elif step <= kv_comm.rank:
k0 = k[half_index0]
v0 = v[half_index0]
bdq, bdk, bdv = _npu_block_backward_with_global_stats(
dout,
q,
k0,
v0,
out,
softmax_lse,
False,
cu_seqlens,
half_cu_seqlens,
softmax_scale,
dropout_p,
window_size,
deterministic,
)
dq += bdq.to(torch.float32)
current_step_dk[half_index0] += bdk.to(torch.float32)
current_step_dv[half_index0] += bdv.to(torch.float32)
else:
bdq, bdk, bdv = _npu_block_backward_with_global_stats(
dout1,
q1,
k,
v,
out1,
softmax_lse1,
False,
half_cu_seqlens,
cu_seqlens,
softmax_scale,
dropout_p,
window_size,
deterministic,
)
dq[half_index1] += bdq.to(torch.float32)
current_step_dk += bdk.to(torch.float32)
current_step_dv += bdv.to(torch.float32)
# K/V gradients are owned by the rank that originally held that shard.
# Rotate the accumulated gradients in the opposite ring direction until
# each owner receives its final dk/dv.
if step == 0:
dk = current_step_dk
dv = current_step_dv
else:
dk = next_dk
dv = next_dv
dk += current_step_dk
dv += current_step_dv
next_dk, next_dv = d_kv_comm.send_recv_kv(dk, dv)
d_kv_comm.wait()
if step + 1 != kv_comm.world_size:
next_k, next_v = kv_comm.send_recv_kv(k, v)
kv_comm.wait()
k, v = next_k, next_v
return dq.to(q.dtype).unsqueeze(0), next_dk.to(q.dtype).unsqueeze(0), next_dv.to(q.dtype).unsqueeze(0)
def npu_forward(
q,
k,
v,
causal,
cu_seqlens_q,
cu_seqlens_kv,
dropout_p,
softmax_scale,
deterministic,
window_size,
):
outputs = _call_npu_fusion_attention(
q,
k,
v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_kv=cu_seqlens_kv,
softmax_scale=softmax_scale,
dropout_p=dropout_p,
causal=causal,
window_size=window_size,
deterministic=deterministic,
)
block_out, softmax_max, softmax_sum = outputs[:3]
block_lse = softmax_max.to(torch.float32) + torch.log(softmax_sum.to(torch.float32))
block_lse = _reshape_npu_lse(block_lse, q.shape[0], q.shape[1])
return block_out, block_lse