fix(fa3): no NaN embeddings with fa_skip_kv_cache under piecewise CUDA graph (#27343)

This commit is contained in:
Jia Guo
2026-06-15 13:46:22 -07:00
committed by GitHub
parent f870bf1ed0
commit 4ed698a491
3 changed files with 203 additions and 4 deletions
@@ -152,6 +152,10 @@ class AlwaysHitShapeEnv:
def __init__(self) -> None:
self.guards: list[Any] = []
# Newer torch Inductor reads ``shape_env.var_to_hint_override`` during
# compilation; provide an empty mapping so this dummy shape env stays
# compatible across torch versions (older torch never accesses it).
self.var_to_hint_override: dict[Any, Any] = {}
def evaluate_guards_expression(self, *args, **kwargs):
return True
@@ -56,6 +56,10 @@ class FlashAttentionMetadata:
cu_seqlens_q: torch.Tensor = None
# Cumulative sequence lengths for key
cu_seqlens_k: torch.Tensor = None
# Dummy-tail varlen metadata for the fa_skip_kv_cache path under a piecewise
# CUDA graph (built once per forward, reused across layers). See forward_extend.
fa_skip_cu_seqlens_q: torch.Tensor = None
fa_skip_max_seqlen_q: int = None
# Window size (typically used by Gemma)
window_size: tuple = (-1, -1)
# Page table, the index of KV Cache Tables/Blocks
@@ -979,14 +983,43 @@ class FlashAttentionBackend(AttentionBackend):
"fa_skip_kv_cache uses raw K/V tensors, "
"FP8 KV cache descaling is not supported in this mode"
)
# Piecewise CUDA graph pads the token dimension up to a captured
# bucket size, so ``q`` has more rows than ``cu_seqlens_q`` covers.
# ``flash_attn_varlen_func`` requires ``q.shape[0] == cu_seqlens_q[-1]``;
# otherwise the boundary query block corrupts the last real token's
# output, producing a NaN embedding (LAST pooling reads that token).
# Append the padded tail as a dummy, self-attending segment so every
# row is a valid sequence. Real tokens stay in their own segment and
# are unaffected. Built once per forward and cached on the metadata
# (reused across layers). ``extend_num_tokens`` is a python int equal
# to ``cu_seqlens_q[-1]`` for extend/prefill forwards, so it needs no
# device sync. With no padding -- or if the count is unavailable (e.g.
# any non-piecewise path) -- cu_seqlens_q already covers q, so fall
# through to using it as-is (no dummy segment).
if metadata.fa_skip_cu_seqlens_q is None:
num_real_tokens = forward_batch.extend_num_tokens
num_padded_tokens = q.shape[0]
if (
num_real_tokens is not None
and num_real_tokens < num_padded_tokens
):
metadata.fa_skip_cu_seqlens_q = torch.cat(
[cu_seqlens_q, cu_seqlens_q.new_tensor([num_padded_tokens])]
)
metadata.fa_skip_max_seqlen_q = max(
int(max_seqlen_q), num_padded_tokens - num_real_tokens
)
else:
metadata.fa_skip_cu_seqlens_q = cu_seqlens_q
metadata.fa_skip_max_seqlen_q = max_seqlen_q
result = flash_attn_varlen_func(
q=q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
k=k.view(-1, layer.tp_k_head_num, layer.head_dim),
v=v.view(-1, layer.tp_v_head_num, layer.v_head_dim),
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_q,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_q,
cu_seqlens_q=metadata.fa_skip_cu_seqlens_q,
cu_seqlens_k=metadata.fa_skip_cu_seqlens_q,
max_seqlen_q=metadata.fa_skip_max_seqlen_q,
max_seqlen_k=metadata.fa_skip_max_seqlen_q,
softmax_scale=layer.scaling,
causal=causal,
window_size=window_size,