e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
407 lines
16 KiB
Python
407 lines
16 KiB
Python
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
"""Shared helpers for attention backend selection and execution."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from typing import Any, Optional, Tuple
|
|
|
|
import torch
|
|
from torch import Tensor
|
|
from torch.nn.functional import scaled_dot_product_attention
|
|
|
|
from ..models._utils import *
|
|
from ..utils.packing import (
|
|
build_sdpa_packed_attention_mask,
|
|
build_xformers_block_causal_mask,
|
|
)
|
|
|
|
if HAS_FLASH_ATTENTION:
|
|
from flash_attn import flash_attn_func, flash_attn_varlen_func
|
|
HAS_XFORMERS = xformers is not None
|
|
|
|
# xformers kernels (FA3, FA2, cutlass) only support compute capability <= 9.0.
|
|
# Disable xformers on newer GPUs (e.g. RTX 5070 Ti / sm_120) and fall back to SDPA.
|
|
if HAS_XFORMERS and torch.cuda.is_available():
|
|
_cc = torch.cuda.get_device_capability()
|
|
if _cc[0] >= 12:
|
|
HAS_XFORMERS = False
|
|
SDPA_HAS_GQA = "enable_gqa" in (scaled_dot_product_attention.__doc__ or "")
|
|
|
|
# PrefixGrouper kernel, resolved once when the env gate is on so PG-off users never load
|
|
# torch flex_attention.
|
|
_flex_shared_prefix_attention = None
|
|
if os.environ.get("UNSLOTH_GRPO_PREFIX_GROUPER", "1").lower() not in ("0", "false", "no", "off"):
|
|
try:
|
|
from .prefix_grouper_kernel import (
|
|
flex_shared_prefix_attention as _flex_shared_prefix_attention,
|
|
)
|
|
except Exception:
|
|
_flex_shared_prefix_attention = None
|
|
|
|
FLASH_VARLEN = "flash_varlen"
|
|
FLASH_DENSE = "flash_dense"
|
|
XFORMERS = "xformers"
|
|
SDPA = "sdpa"
|
|
|
|
|
|
XFORMERS_BLOCK_DIAG_CLS = xformers.attn_bias.BlockDiagonalCausalMask if HAS_XFORMERS else None
|
|
|
|
|
|
@dataclass
|
|
class AttentionConfig:
|
|
"""
|
|
Per-layer attention metadata.
|
|
|
|
NOTE(djsaunde): Constructed on every forward pass (not once per layer) since
|
|
it can be invalid across passes (e.g. switching training/inference). Kept
|
|
separate from AttentionContext to group params.
|
|
"""
|
|
|
|
backend: str
|
|
n_kv_heads: int
|
|
n_groups: int
|
|
flash_dense_kwargs: Optional[dict[str, Any]] = None
|
|
flash_varlen_kwargs: Optional[dict[str, Any]] = None
|
|
sdpa_kwargs: Optional[dict[str, Any]] = None
|
|
xformers_kwargs: Optional[dict[str, Any]] = None
|
|
|
|
|
|
@dataclass
|
|
class AttentionContext:
|
|
"""Per-call info required to run attention."""
|
|
|
|
bsz: int
|
|
q_len: int
|
|
kv_seq_len: int
|
|
n_heads: int
|
|
head_dim: int
|
|
requires_grad: bool
|
|
seq_info: Optional[Tuple[Tensor, Tensor, int]]
|
|
attention_mask: Optional[Tensor]
|
|
causal_mask: Optional[Any]
|
|
sliding_window: Optional[int] = None
|
|
# PrefixGrouper: non-None routes Q/K/V through the FlexAttention shared-prefix kernel;
|
|
# None leaves every existing construction/behavior unchanged.
|
|
prefix_seg_info: Optional[Any] = None
|
|
|
|
|
|
def select_attention_backend(use_varlen: bool = False) -> str:
|
|
"""Return attention backend based on availability / priority order."""
|
|
|
|
if HAS_FLASH_ATTENTION:
|
|
if use_varlen:
|
|
return FLASH_VARLEN
|
|
else:
|
|
return FLASH_DENSE
|
|
if HAS_XFORMERS:
|
|
return XFORMERS
|
|
return SDPA
|
|
|
|
|
|
def resolve_prefix_seg_info(kwargs, past_key_value, attention_mask):
|
|
"""PrefixGrouper shared-prefix segment table resolver for the arch attention forwards.
|
|
|
|
The GRPO PrefixGrouper packed path rides a ``PrefixSegInfo`` in through ``**kwargs``
|
|
(same route as ``packed_seq_lengths``). When present, the forward must route Q/K/V
|
|
through the FlexAttention shared-prefix kernel via ``AttentionContext.prefix_seg_info``.
|
|
|
|
Returns the seg table (or ``None`` when PrefixGrouper did not group this batch -- the
|
|
unchanged path). Hardened: the shared-prefix stream is NOT a plain causal sequence, so running
|
|
it under a KV cache or an explicit padding mask would silently produce wrong logprobs.
|
|
That combination can only arise from misuse (PrefixGrouper only rides in via the GRPO
|
|
logprob forward, which is mask-free prefill), so we RAISE loudly instead of degrading
|
|
to a wrong result.
|
|
|
|
Factored here so every arch (llama/mistral/qwen3/gemma2/cohere/granite/falcon_h1)
|
|
shares one implementation and cannot drift.
|
|
"""
|
|
seg = kwargs.get("prefix_seg_info", None)
|
|
if seg is not None and (past_key_value is not None or attention_mask is not None):
|
|
raise RuntimeError(
|
|
"PrefixGrouper: prefix_seg_info requires prefill with no KV cache and no "
|
|
f"attention_mask (got past_key_value={past_key_value is not None}, "
|
|
f"attention_mask={attention_mask is not None})."
|
|
)
|
|
return seg
|
|
|
|
|
|
def run_attention(
|
|
*, config: AttentionConfig, context: AttentionContext, Q: Tensor, K: Tensor, V: Tensor
|
|
) -> Tensor:
|
|
"""
|
|
Run attention using config / context info.
|
|
|
|
Backend priority (speed): FlashAttention if installed (varlen for packed
|
|
inputs with `seq_info`, else dense), then xFormers, then SDPA as fallback.
|
|
Varlen flash is preferred for packed batches as it avoids padding; xFormers
|
|
and SDPA handle packing via a block-diagonal mask.
|
|
"""
|
|
|
|
# PrefixGrouper shared-prefix attention (GRPO dedup). Q/K/V here are [bsz, H, T, D];
|
|
# the kernel takes/returns [1, T, H, D], matching the other backends. The field is
|
|
# only set when the env gate is on and grouping succeeded; None keeps every backend
|
|
# byte-identical.
|
|
if context.prefix_seg_info is not None:
|
|
flex_shared_prefix_attention = _flex_shared_prefix_attention
|
|
if flex_shared_prefix_attention is None:
|
|
# gate flipped on after import (or one-time load failed): resolve lazily.
|
|
from ..utils.prefix_grouper_kernel import flex_shared_prefix_attention
|
|
|
|
scale = None
|
|
if config.flash_varlen_kwargs:
|
|
scale = config.flash_varlen_kwargs.get("softmax_scale")
|
|
A = flex_shared_prefix_attention(
|
|
Q.transpose(1, 2),
|
|
K.transpose(1, 2),
|
|
V.transpose(1, 2),
|
|
context.prefix_seg_info,
|
|
scale = scale,
|
|
)
|
|
return A # [1, T, n_heads, head_dim]
|
|
|
|
backend = config.backend
|
|
if backend == FLASH_VARLEN and context.seq_info is None:
|
|
backend = FLASH_DENSE if HAS_FLASH_ATTENTION else SDPA
|
|
|
|
# [TODO] Flash/xFormers don't support arbitrary attn masks; with a padding
|
|
# mask present (e.g. left-padded generation), fall back to SDPA.
|
|
if context.attention_mask is not None and backend in (
|
|
FLASH_DENSE,
|
|
FLASH_VARLEN,
|
|
XFORMERS,
|
|
):
|
|
backend = SDPA
|
|
|
|
flash_dense_kwargs = config.flash_dense_kwargs or {}
|
|
flash_varlen_kwargs = config.flash_varlen_kwargs or {}
|
|
sdpa_kwargs = config.sdpa_kwargs or {}
|
|
xformers_kwargs = config.xformers_kwargs or {}
|
|
|
|
bsz = context.bsz
|
|
n_heads = context.n_heads
|
|
q_len = context.q_len
|
|
head_dim = context.head_dim
|
|
kv_seq_len = context.kv_seq_len
|
|
requires_grad = context.requires_grad
|
|
sliding_window = context.sliding_window
|
|
|
|
# DoRA promotes q/k/v_proj outputs to fp32, which FlashAttention rejects, so
|
|
# downcast any fp32 Q/K/V to a flash-supported dtype (#1013).
|
|
if backend in (FLASH_DENSE, FLASH_VARLEN) and torch.float32 in (
|
|
Q.dtype,
|
|
K.dtype,
|
|
V.dtype,
|
|
):
|
|
# Prefer the autocast dtype, else a non-fp32 input's dtype, then clamp.
|
|
if torch.is_autocast_enabled():
|
|
try:
|
|
flash_dtype = torch.get_autocast_dtype("cuda")
|
|
except (AttributeError, TypeError):
|
|
flash_dtype = torch.get_autocast_gpu_dtype()
|
|
else:
|
|
flash_dtype = next((d for d in (Q.dtype, K.dtype, V.dtype) if d != torch.float32), None)
|
|
if flash_dtype not in (torch.float16, torch.bfloat16):
|
|
flash_dtype = torch.bfloat16 if SUPPORTS_BFLOAT16 else torch.float16
|
|
Q, K, V = Q.to(flash_dtype), K.to(flash_dtype), V.to(flash_dtype)
|
|
|
|
if backend == FLASH_VARLEN:
|
|
Q_f = Q.transpose(1, 2).reshape(bsz * q_len, n_heads, head_dim)
|
|
K_f = K.transpose(1, 2).reshape(bsz * q_len, config.n_kv_heads, head_dim)
|
|
V_f = V.transpose(1, 2).reshape(bsz * q_len, config.n_kv_heads, head_dim)
|
|
_, cu_seqlens, max_seqlen = context.seq_info
|
|
return flash_attn_varlen_func(
|
|
Q_f,
|
|
K_f,
|
|
V_f,
|
|
cu_seqlens,
|
|
cu_seqlens,
|
|
max_seqlen,
|
|
max_seqlen,
|
|
**flash_varlen_kwargs,
|
|
).view(bsz, q_len, n_heads, head_dim)
|
|
elif backend == FLASH_DENSE:
|
|
Q_t = Q.transpose(1, 2)
|
|
K_t = K.transpose(1, 2)
|
|
V_t = V.transpose(1, 2)
|
|
return flash_attn_func(Q_t, K_t, V_t, **flash_dense_kwargs).reshape(
|
|
bsz, q_len, n_heads, head_dim
|
|
)
|
|
elif backend == XFORMERS:
|
|
attn_bias = build_xformers_block_causal_mask(
|
|
context.seq_info,
|
|
sliding_window = sliding_window,
|
|
base_mask = context.causal_mask,
|
|
)
|
|
|
|
Q_t = Q.transpose(1, 2)
|
|
K_t = K.transpose(1, 2)
|
|
V_t = V.transpose(1, 2)
|
|
|
|
K_mod = K_t
|
|
V_mod = V_t
|
|
Q_mod = Q_t
|
|
|
|
if config.n_groups != 1:
|
|
K_mod = K_t.view(bsz, kv_seq_len, config.n_kv_heads, 1, head_dim)
|
|
V_mod = V_t.view(bsz, kv_seq_len, config.n_kv_heads, 1, head_dim)
|
|
K_mod = K_mod.expand(bsz, kv_seq_len, config.n_kv_heads, config.n_groups, head_dim)
|
|
V_mod = V_mod.expand(bsz, kv_seq_len, config.n_kv_heads, config.n_groups, head_dim)
|
|
|
|
if requires_grad:
|
|
K_mod = K_mod.reshape(bsz, kv_seq_len, n_heads, head_dim)
|
|
V_mod = V_mod.reshape(bsz, kv_seq_len, n_heads, head_dim)
|
|
else:
|
|
Q_mod = Q_t.view(bsz, q_len, config.n_kv_heads, config.n_groups, head_dim)
|
|
|
|
has_block = XFORMERS_BLOCK_DIAG_CLS is not None and isinstance(
|
|
attn_bias, XFORMERS_BLOCK_DIAG_CLS
|
|
)
|
|
|
|
if config.n_groups != 1 and has_block:
|
|
if not requires_grad:
|
|
Q_mod = Q_mod.view(1, bsz * q_len, config.n_kv_heads, config.n_groups, head_dim)
|
|
K_mod = K_mod.view(
|
|
1, bsz * kv_seq_len, config.n_kv_heads, config.n_groups, head_dim
|
|
)
|
|
V_mod = V_mod.view(
|
|
1, bsz * kv_seq_len, config.n_kv_heads, config.n_groups, head_dim
|
|
)
|
|
else:
|
|
Q_mod = Q_mod.view(1, bsz * q_len, n_heads, head_dim)
|
|
K_mod = K_mod.view(1, bsz * kv_seq_len, n_heads, head_dim)
|
|
V_mod = V_mod.view(1, bsz * kv_seq_len, n_heads, head_dim)
|
|
|
|
out = xformers_attention(
|
|
Q_mod,
|
|
K_mod,
|
|
V_mod,
|
|
attn_bias = attn_bias,
|
|
**xformers_kwargs,
|
|
)
|
|
|
|
if config.n_groups != 1 and not requires_grad:
|
|
out = out.view(bsz, q_len, config.n_kv_heads, config.n_groups, head_dim)
|
|
out = out.reshape(bsz, q_len, n_heads, head_dim)
|
|
else:
|
|
out = out.view(bsz, q_len, n_heads, head_dim)
|
|
return out
|
|
else:
|
|
local_mask = context.attention_mask
|
|
is_causal_local = False
|
|
if context.seq_info is not None and local_mask is None:
|
|
local_mask = build_sdpa_packed_attention_mask(
|
|
context.seq_info,
|
|
dtype = Q.dtype,
|
|
device = Q.device,
|
|
sliding_window = sliding_window,
|
|
)
|
|
else:
|
|
q_len_local = Q.shape[-2]
|
|
k_len_local = K.shape[-2]
|
|
# ---- SDPA mask normalization for left padding / 2D masks ----
|
|
if local_mask is not None and isinstance(local_mask, torch.Tensor):
|
|
local_mask = local_mask.to(device = Q.device)
|
|
|
|
if local_mask.dim() == 2:
|
|
# key padding keep mask: (bsz, k_len), 1/True = real token
|
|
if local_mask.dtype == torch.bool:
|
|
key_keep = local_mask
|
|
else:
|
|
# tokenizer attention_mask is typically int 0/1
|
|
key_keep = local_mask != 0
|
|
|
|
past_len = k_len_local - q_len_local # works for prefill (0) and decode
|
|
q_pos = torch.arange(past_len, past_len + q_len_local, device = Q.device)
|
|
k_pos = torch.arange(k_len_local, device = Q.device)
|
|
|
|
causal_keep = k_pos[None, :] <= q_pos[:, None] # True = allowed (SDPA)
|
|
if sliding_window is not None:
|
|
causal_keep &= k_pos[None, :] >= (q_pos[:, None] - (sliding_window - 1))
|
|
|
|
# (bsz, 1, q_len, k_len) boolean keep mask
|
|
local_mask = causal_keep[None, None, :, :] & key_keep[:, None, None, :]
|
|
|
|
elif local_mask.dim() == 3:
|
|
# (bsz, q_len, k_len) -> (bsz, 1, q_len, k_len)
|
|
local_mask = local_mask[:, None, :, :]
|
|
|
|
elif local_mask.dim() == 4:
|
|
if local_mask.dtype != torch.bool:
|
|
# Use boolean keep masks for better SDPA stability.
|
|
local_mask = local_mask.eq(0)
|
|
else:
|
|
raise ValueError(f"Unsupported SDPA attention_mask rank: {local_mask.dim()}")
|
|
|
|
# Avoid NaNs from fully-masked rows (common with left padding).
|
|
if local_mask.dtype == torch.bool:
|
|
no_allowed = ~local_mask.any(dim = -1, keepdim = True) # (bsz,1,q_len,1)
|
|
local_mask = local_mask | no_allowed
|
|
|
|
is_causal_local = local_mask is None and q_len_local == k_len_local
|
|
|
|
kwargs = dict(sdpa_kwargs)
|
|
kwargs.setdefault("attn_mask", local_mask)
|
|
kwargs.setdefault("is_causal", is_causal_local)
|
|
|
|
use_sdpa_gqa = SDPA_HAS_GQA and config.n_groups != 1
|
|
if (
|
|
use_sdpa_gqa
|
|
and (not requires_grad)
|
|
and isinstance(local_mask, torch.Tensor)
|
|
and local_mask.dim() >= 3
|
|
and local_mask.shape[0] > 1
|
|
):
|
|
# Batched masked inference has shown row-coupled drift with SDPA GQA.
|
|
# Fall back to explicit KV expansion for deterministic row-wise behavior.
|
|
use_sdpa_gqa = False
|
|
|
|
if use_sdpa_gqa:
|
|
kwargs.setdefault("enable_gqa", True)
|
|
out = scaled_dot_product_attention(Q, K, V, **kwargs)
|
|
return out.transpose(1, 2)
|
|
|
|
K_mod = K
|
|
V_mod = V
|
|
if config.n_groups != 1:
|
|
K_mod = K[:, :, None, :, :].expand(
|
|
bsz, config.n_kv_heads, config.n_groups, kv_seq_len, head_dim
|
|
)
|
|
V_mod = V[:, :, None, :, :].expand(
|
|
bsz, config.n_kv_heads, config.n_groups, kv_seq_len, head_dim
|
|
)
|
|
K_mod = K_mod.reshape(bsz, n_heads, kv_seq_len, head_dim)
|
|
V_mod = V_mod.reshape(bsz, n_heads, kv_seq_len, head_dim)
|
|
|
|
out = scaled_dot_product_attention(
|
|
Q.contiguous(),
|
|
K_mod.contiguous(),
|
|
V_mod.contiguous(),
|
|
**kwargs,
|
|
)
|
|
return out.transpose(1, 2).contiguous()
|
|
|
|
|
|
__all__ = [
|
|
"AttentionConfig",
|
|
"AttentionContext",
|
|
"select_attention_backend",
|
|
"resolve_prefix_seg_info",
|
|
"run_attention",
|
|
]
|