Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1868,29 +1868,45 @@ def backward(ctx, d_out, *_args):
dq = dq[..., : d_out.shape[-1]]
dk = dk[..., : d_out.shape[-1]]
dv = dv[..., : d_out.shape[-1]]
# Zero-fill positions beyond cu_seqlens_*_padded[-1] in dQ/dK/dV for THD.
# Use Q's padded boundary for dQ and KV's padded boundary for dK/dV.
# Sync-free `arange + mask` so capture and eager paths run the same code.
_qkv_format = ctx.qkv_layout.split("_")[0].replace("3", "").replace("2", "")
if _qkv_format == "thd":
if (
cu_seqlens_q_padded is not None
and isinstance(dq, torch.Tensor)
and dq.shape[0] > 0
):
q_pad_mask = (
torch.arange(dq.shape[0], device=dq.device) >= cu_seqlens_q_padded[-1]
)
dq[q_pad_mask] = 0
if cu_seqlens_kv_padded is not None:
kv_actual_t = cu_seqlens_kv_padded[-1]
for d_tensor in (dk, dv):
if isinstance(d_tensor, torch.Tensor) and d_tensor.shape[0] > 0:
kv_pad_mask = (
torch.arange(d_tensor.shape[0], device=d_tensor.device)
>= kv_actual_t
)
d_tensor[kv_pad_mask] = 0
if is_graph_capturing():
if (
cu_seqlens_q_padded is not None
and isinstance(dq, torch.Tensor)
and dq.shape[0] > 0
):
q_pad_mask = (
torch.arange(dq.shape[0], device=dq.device) >= cu_seqlens_q_padded[-1]
)
dq[q_pad_mask] = 0
if cu_seqlens_kv_padded is not None:
kv_actual_t = cu_seqlens_kv_padded[-1]
for d_tensor in (dk, dv):
if isinstance(d_tensor, torch.Tensor) and d_tensor.shape[0] > 0:
kv_pad_mask = (
torch.arange(d_tensor.shape[0], device=d_tensor.device)
>= kv_actual_t
)
d_tensor[kv_pad_mask] = 0
else:
if (
cu_seqlens_q_padded is not None
and isinstance(dq, torch.Tensor)
and dq.shape[0] > 0
):
q_end = int(cu_seqlens_q_padded[-1])
if q_end < dq.shape[0]:
dq[q_end:].fill_(0)
if cu_seqlens_kv_padded is not None:
kv_end = int(cu_seqlens_kv_padded[-1])
for d_tensor in (dk, dv):
if (
isinstance(d_tensor, torch.Tensor)
and d_tensor.shape[0] > 0
and kv_end < d_tensor.shape[0]
):
d_tensor[kv_end:].fill_(0)
else:
with get_nvtx_range_context("FusedAttnFunc.backward"):
# get nominal data type of dq, dk, dv
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3109,8 +3109,6 @@ def backward(ctx, dout, *_args):
and cu_seqlens_kv_padded is not None
):
if is_graph_capturing():
# arange+mask under capture: `tensor[scalar_tensor:]` slicing would
# force a GPU->CPU sync that is forbidden during CUDA graph capture.
q_pad_mask = torch.arange(dq.shape[0], device=dq.device) >= cu_seqlens_q_padded[-1]
kv_pad_mask = (
torch.arange(dk.shape[0], device=dk.device) >= cu_seqlens_kv_padded[-1]
Expand All @@ -3119,10 +3117,14 @@ def backward(ctx, dout, *_args):
dk[kv_pad_mask] = 0
dv[kv_pad_mask] = 0
else:
# Pre-existing TE eager-mode behaviour.
dq[cu_seqlens_q_padded[-1] :].fill_(0)
dk[cu_seqlens_kv_padded[-1] :].fill_(0)
dv[cu_seqlens_kv_padded[-1] :].fill_(0)
q_end = int(cu_seqlens_q_padded[-1])
kv_end = int(cu_seqlens_kv_padded[-1])
if q_end < dq.shape[0]:
dq[q_end:].fill_(0)
if kv_end < dk.shape[0]:
dk[kv_end:].fill_(0)
if kv_end < dv.shape[0]:
dv[kv_end:].fill_(0)

if ctx.fp8 and ctx.is_input_fp8:
dq, dk, dv, _, _ = combine_and_quantize(ctx.qkv_layout, dq, dk, dv, ctx.dQKV_quantizer)
Expand All @@ -3140,9 +3142,7 @@ def backward(ctx, dout, *_args):
ctx.dP_quantizer,
)

# Partial-gradient reduction can write THD inter-sequence padding.
# Clean it while gradients and per-step sequence metadata share sequence order.
if ctx.qkv_format == "thd":
if ctx.qkv_format == "thd" and ctx.pad_between_seqs:
_zero_thd_padding((dq,), cu_seqlens_q_per_step[0], cu_seqlens_q_padded)
_zero_thd_padding((dk, dv), cu_seqlens_kv_per_step[0], cu_seqlens_kv_padded)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,29 @@
_THD_POLICY_VALIDATION_CACHE_LIMIT = 64
_thd_policy_validation_cache = []

_PAD_BETWEEN_SEQS_EAGER_CACHE_LIMIT = 64
_pad_between_seqs_eager_cache = {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functools.lru_cache would be simpler and less error-prone than implementing the cache from scratch. That said, we already follow that anti-pattern with _thd_policy_validation_cache, so there's something to be said about maintaining consistency.



def _pad_between_seqs_cache_key(
cu_seqlens_q, cu_seqlens_kv, cu_seqlens_q_padded, cu_seqlens_kv_padded
):
def _key(t):
if t is None:
return (0, 0, 0, 0)
try:
version = t._version
except AttributeError:
version = 0
return (t.data_ptr(), tuple(t.shape), t.dtype, version)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to assume that two tensors with the same buffer will have the same padding requirements? If cu_seqlens_* are not persistent, the same buffers may be reused in different steps or different layers. Perhaps it would be safer to do something like:

return (id(t), version)


return (
_key(cu_seqlens_q),
_key(cu_seqlens_kv),
_key(cu_seqlens_q_padded),
_key(cu_seqlens_kv_padded),
)


def _get_thd_policy_attention_backend(
policy: Dict[str, Any],
Expand Down Expand Up @@ -2706,24 +2729,47 @@ def forward(
else None
)

# Default pad_between_seqs auto-detect. For THD, infer presence of
# inter-sequence padding from whether padded cu_seqlens were supplied --
# sync-free, and stable across eager and CUDA graph capture (the auto-detect
# must return the same value in both modes for backend selection to match).
# If padded cu_seqlens are the *same object* as the unpadded ones, no real
# inter-sequence padding exists (only THD tail padding) -- treat as False so
# FlashAttention v2/v4 remain eligible.
if pad_between_seqs is None:
if qkv_format == "thd":
if (
cu_seqlens_q_padded is cu_seqlens_q
and cu_seqlens_kv_padded is cu_seqlens_kv
):
pad_between_seqs = False
if is_graph_capturing():
cache_key = _pad_between_seqs_cache_key(
cu_seqlens_q,
cu_seqlens_kv,
cu_seqlens_q_padded,
cu_seqlens_kv_padded,
)
if cache_key in _pad_between_seqs_eager_cache:
pad_between_seqs = _pad_between_seqs_eager_cache[cache_key]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that the cache is not populated during graph capture. In order to take advantage of the cache, we assume that the cache is populated in the warmup steps.

elif (
cu_seqlens_q_padded is cu_seqlens_q
and cu_seqlens_kv_padded is cu_seqlens_kv
):
pad_between_seqs = False
else:
pad_between_seqs = (
cu_seqlens_q_padded is not None or cu_seqlens_kv_padded is not None
)
else:
Comment thread
greptile-apps[bot] marked this conversation as resolved.
pad_between_seqs = (
cu_seqlens_q_padded is not None or cu_seqlens_kv_padded is not None
cu_seqlens_q_padded is not None
and not torch.equal(cu_seqlens_q_padded[:-1], cu_seqlens_q[:-1])
) or (
cu_seqlens_kv_padded is not None
and not torch.equal(cu_seqlens_kv_padded[:-1], cu_seqlens_kv[:-1])
)
cache_key = _pad_between_seqs_cache_key(
cu_seqlens_q,
cu_seqlens_kv,
cu_seqlens_q_padded,
cu_seqlens_kv_padded,
)
_pad_between_seqs_eager_cache[cache_key] = pad_between_seqs
while (
len(_pad_between_seqs_eager_cache) > _PAD_BETWEEN_SEQS_EAGER_CACHE_LIMIT
):
_pad_between_seqs_eager_cache.pop(
next(iter(_pad_between_seqs_eager_cache))
)
else:
pad_between_seqs = False

Expand Down