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
437 lines
17 KiB
Python
437 lines
17 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 Affero 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 Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
"""FlexAttention shared-prefix kernel for PrefixGrouper (GRPO shared-prompt dedup).
|
|
|
|
In GRPO every prompt spawns ``G = num_generations`` completions that share the same
|
|
prompt prefix. The full-row packed path forwards the identical prefix ``G`` times.
|
|
PrefixGrouper stores the prefix ONCE and concatenates only the ``G`` suffixes, with an
|
|
attention layout where each suffix token attends to ``[the single shared prefix] +
|
|
[causal within its own suffix]``. This kernel expresses that one-prefix -> many-suffix
|
|
fan-out via a ``torch.nn.attention.flex_attention`` block mask, so the masked-out
|
|
cross-suffix / cross-group blocks are never computed and the ``P + G*R`` FLOP saving is
|
|
realised (not merely a masked dense ``O(T^2)``).
|
|
|
|
Mask semantics (identical to the certified SDPA oracle):
|
|
|
|
keep(q_idx, kv_idx) = same_group(q, kv) AND
|
|
( is_prefix[kv_idx] # full prefix visibility
|
|
OR ( suffix_of_kv[kv_idx] == suffix_of_kv[q_idx] # same suffix ...
|
|
AND kv_idx <= q_idx ) ) # ... causal within it
|
|
|
|
This module is self-contained (no dependency on any temp/ scratch dir) so PrefixGrouper
|
|
works from the installed source after a fresh compile. It is only imported lazily from
|
|
``attention_dispatch.run_attention`` when ``prefix_seg_info`` is present, which itself is
|
|
only ever set when ``UNSLOTH_GRPO_PREFIX_GROUPER`` is on and grouping succeeded, so the
|
|
default (off) path never touches this file.
|
|
|
|
Provided entry points:
|
|
* ``PrefixSegInfo`` : per-flat-token segment metadata + cache signature.
|
|
* ``build_seg_info_multigroup``: build PrefixSegInfo for many groups packed flat.
|
|
* ``build_seg_info_from_layout``: build PrefixSegInfo for ONE group (test helper).
|
|
* ``get_block_mask`` : cached create_block_mask keyed on the signature.
|
|
* ``flex_shared_prefix_attention(Q, K, V, prefix_seg_info)``
|
|
Q/K/V of shape [1, T, n_heads, head_dim]; returns [1, T, n_heads, head_dim],
|
|
IDENTICAL semantics to the SDPA oracle.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
import torch
|
|
from torch.nn.attention.flex_attention import (
|
|
BlockMask,
|
|
create_block_mask,
|
|
flex_attention,
|
|
)
|
|
|
|
# GRPO feeds many distinct segment lengths; at dynamo's default recompile_limit (8) the
|
|
# compiled kernel silently reuses a mismatched specialisation (wrong results). Raise it.
|
|
torch._dynamo.config.recompile_limit = max(getattr(torch._dynamo.config, "recompile_limit", 8), 256)
|
|
torch._dynamo.config.accumulated_recompile_limit = max(
|
|
getattr(torch._dynamo.config, "accumulated_recompile_limit", 256), 2048
|
|
)
|
|
|
|
|
|
# Compiled kernels: torch.compile fuses the sparse mask into one kernel. dynamic=True is
|
|
# required: T changes almost every GRPO batch and dynamic=False recompiles per T (~14s
|
|
# each). T is still padded to a multiple of 128 (_pad_len) for the backward kernel.
|
|
_flex_attention_compiled = torch.compile(flex_attention, dynamic = True)
|
|
_create_block_mask_compiled = torch.compile(create_block_mask, dynamic = True)
|
|
|
|
# Flash block sizes by Q dtype (env-overridable). The two disjoint key runs (prefix +
|
|
# own-suffix) stress online-softmax accumulation: fp32 needs 32/32 for a ~1e-6 floor;
|
|
# bf16 passes parity at 128/64 and is ~5x faster (128/128 OOMs Triton on B200).
|
|
_FP32_BLOCK_M = int(os.environ.get("PG_FLEX_BLOCK_M", "32"))
|
|
_FP32_BLOCK_N = int(os.environ.get("PG_FLEX_BLOCK_N", "32"))
|
|
_BF16_BLOCK_M = int(os.environ.get("PG_FLEX_BF16_BLOCK_M", "128"))
|
|
_BF16_BLOCK_N = int(os.environ.get("PG_FLEX_BF16_BLOCK_N", "64"))
|
|
|
|
|
|
def _kernel_options_for_dtype(dtype):
|
|
"""Pick the numerically-safe flash block sizes for the Q dtype."""
|
|
if dtype == torch.bfloat16 or dtype == torch.float16:
|
|
return {"BLOCK_M": _BF16_BLOCK_M, "BLOCK_N": _BF16_BLOCK_N}
|
|
return {"BLOCK_M": _FP32_BLOCK_M, "BLOCK_N": _FP32_BLOCK_N}
|
|
|
|
|
|
# Backward-compat constant (fp32 default).
|
|
_FLEX_KERNEL_OPTIONS = {"BLOCK_M": _FP32_BLOCK_M, "BLOCK_N": _FP32_BLOCK_N}
|
|
|
|
# The compiled backward trips an Inductor assertion when T is not a multiple of 128, so
|
|
# pad the flat sequence. Pad tokens form a group that attends to / is attended by nothing
|
|
# (all-masked rows return 0, not NaN) and are sliced off the output.
|
|
_PAD_MULTIPLE = 128
|
|
_PAD_GROUP = -99 # sentinel group id / suffix id for pad tokens
|
|
|
|
|
|
def _pad_len(T: int) -> int:
|
|
return ((T + _PAD_MULTIPLE - 1) // _PAD_MULTIPLE) * _PAD_MULTIPLE
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Segment metadata
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class PrefixSegInfo:
|
|
"""Per-flat-token segment metadata driving the shared-prefix block mask.
|
|
|
|
The label tensors are 1-D of length ``T_pad`` (>= real ``T``, padded up to a multiple
|
|
of 128 so the backward kernel compiles). Positions ``[T:T_pad)`` are pad tokens
|
|
(group/suffix == _PAD_GROUP) that attend to nothing.
|
|
|
|
Attributes
|
|
----------
|
|
group_of_kv : LongTensor [T_pad]
|
|
Group id per flat token (0..num_groups-1); _PAD_GROUP for pad tokens.
|
|
is_prefix : BoolTensor [T_pad]
|
|
True iff the token is a prefix token of its group (False for pad).
|
|
suffix_of_kv : LongTensor [T_pad]
|
|
Suffix id per flat token; -1 for prefix, _PAD_GROUP for pad. Suffix ids are
|
|
globally unique across groups.
|
|
signature : hashable
|
|
Cache key for the block mask (depends only on the labels + T_pad).
|
|
T : int
|
|
Real flat sequence length (Q/K/V of this length are padded internally).
|
|
T_pad : int
|
|
Padded length (multiple of 128) at which the block mask is built.
|
|
"""
|
|
|
|
group_of_kv: torch.Tensor
|
|
is_prefix: torch.Tensor
|
|
suffix_of_kv: torch.Tensor
|
|
signature: Tuple
|
|
T: int
|
|
T_pad: int
|
|
|
|
|
|
def _pad_labels(group_of_kv, is_prefix, suffix_of_kv, device):
|
|
"""Pad the label tensors up to a multiple of 128 with pad-token sentinels."""
|
|
T = int(group_of_kv.numel())
|
|
T_pad = _pad_len(T)
|
|
if T_pad == T:
|
|
return group_of_kv, is_prefix, suffix_of_kv, T, T_pad
|
|
pad = T_pad - T
|
|
group_of_kv = torch.cat(
|
|
[group_of_kv, torch.full((pad,), _PAD_GROUP, dtype = torch.long, device = device)]
|
|
)
|
|
is_prefix = torch.cat([is_prefix, torch.zeros(pad, dtype = torch.bool, device = device)])
|
|
suffix_of_kv = torch.cat(
|
|
[suffix_of_kv, torch.full((pad,), _PAD_GROUP, dtype = torch.long, device = device)]
|
|
)
|
|
return group_of_kv, is_prefix, suffix_of_kv, T, T_pad
|
|
|
|
|
|
def build_seg_info_from_layout(layout, device: Optional[torch.device] = None) -> PrefixSegInfo:
|
|
"""Build PrefixSegInfo for ONE group from an object with ``.flat_ids``, ``.P`` and
|
|
``.suffix_slices`` (used by the parity test / oracle helpers)."""
|
|
if device is None:
|
|
device = layout.flat_ids.device
|
|
T = int(layout.flat_ids.shape[1])
|
|
P = int(layout.P)
|
|
|
|
group_of_kv = torch.zeros(T, dtype = torch.long, device = device) # single group -> 0
|
|
is_prefix = torch.zeros(T, dtype = torch.bool, device = device)
|
|
is_prefix[:P] = True
|
|
suffix_of_kv = torch.full((T,), -1, dtype = torch.long, device = device)
|
|
for i, (s, e) in enumerate(layout.suffix_slices):
|
|
suffix_of_kv[s:e] = i
|
|
|
|
group_of_kv, is_prefix, suffix_of_kv, T, T_pad = _pad_labels(
|
|
group_of_kv, is_prefix, suffix_of_kv, device
|
|
)
|
|
sig = ("single", T_pad, P, tuple((s, e) for (s, e) in layout.suffix_slices))
|
|
return PrefixSegInfo(
|
|
group_of_kv = group_of_kv,
|
|
is_prefix = is_prefix,
|
|
suffix_of_kv = suffix_of_kv,
|
|
signature = sig,
|
|
T = T,
|
|
T_pad = T_pad,
|
|
)
|
|
|
|
|
|
def build_seg_info_multigroup(
|
|
group_specs: List[Tuple[int, List[int]]], device: torch.device
|
|
) -> Tuple[PrefixSegInfo, List[dict]]:
|
|
"""Build PrefixSegInfo for several shared-prefix groups packed block-diagonally.
|
|
|
|
Parameters
|
|
----------
|
|
group_specs : list of (P_g, [R_{g,0}, R_{g,1}, ...])
|
|
For each group: prefix length and the list of suffix lengths.
|
|
|
|
Returns
|
|
-------
|
|
seg : PrefixSegInfo
|
|
group_meta : list of dicts with 'base', 'P', 'prefix_last_index', 'suffix_slices'
|
|
(flat offsets), enough to build the completion index map.
|
|
"""
|
|
group_of_list = []
|
|
is_prefix_list = []
|
|
suffix_of_list = []
|
|
group_meta = []
|
|
|
|
base = 0
|
|
suffix_counter = 0
|
|
sig_parts = []
|
|
for gid, (P, R_list) in enumerate(group_specs):
|
|
# prefix
|
|
group_of_list.append(torch.full((P,), gid, dtype = torch.long, device = device))
|
|
is_prefix_list.append(torch.ones(P, dtype = torch.bool, device = device))
|
|
suffix_of_list.append(torch.full((P,), -1, dtype = torch.long, device = device))
|
|
prefix_last_index = base + P - 1
|
|
suffix_slices = []
|
|
cursor = base + P
|
|
for r in R_list:
|
|
group_of_list.append(torch.full((r,), gid, dtype = torch.long, device = device))
|
|
is_prefix_list.append(torch.zeros(r, dtype = torch.bool, device = device))
|
|
suffix_of_list.append(torch.full((r,), suffix_counter, dtype = torch.long, device = device))
|
|
suffix_slices.append((cursor, cursor + r))
|
|
cursor += r
|
|
suffix_counter += 1
|
|
group_meta.append(
|
|
{
|
|
"base": base,
|
|
"P": P,
|
|
"prefix_last_index": prefix_last_index,
|
|
"suffix_slices": suffix_slices,
|
|
}
|
|
)
|
|
sig_parts.append((P, tuple(R_list)))
|
|
base = cursor
|
|
|
|
group_of_kv = torch.cat(group_of_list)
|
|
is_prefix = torch.cat(is_prefix_list)
|
|
suffix_of_kv = torch.cat(suffix_of_list)
|
|
group_of_kv, is_prefix, suffix_of_kv, T, T_pad = _pad_labels(
|
|
group_of_kv, is_prefix, suffix_of_kv, device
|
|
)
|
|
sig = ("multi", T_pad, tuple(sig_parts))
|
|
seg = PrefixSegInfo(
|
|
group_of_kv = group_of_kv,
|
|
is_prefix = is_prefix,
|
|
suffix_of_kv = suffix_of_kv,
|
|
signature = sig,
|
|
T = T,
|
|
T_pad = T_pad,
|
|
)
|
|
return seg, group_meta
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Block-mask builder + cache, keyed on (signature, device): the mask depends only on the
|
|
# per-token labels and T, so it is reused across layers and steps.
|
|
|
|
_BLOCK_MASK_CACHE: Dict[Tuple, BlockMask] = {}
|
|
|
|
|
|
def _make_mask_mod(group_of_kv, is_prefix, suffix_of_kv):
|
|
"""Return a mask_mod closure over the (device) label tensors.
|
|
|
|
keep(q, kv) = same_group AND
|
|
( is_prefix[kv] AND kv <= q # causal within/ into prefix
|
|
OR ( suffix_of_kv[kv] == suffix_of_kv[q] # same suffix ...
|
|
AND (not is_prefix[q]) # q is a suffix token ...
|
|
AND kv <= q ) ) # ... causal within it
|
|
|
|
The single ``kv <= q`` guard on the is_prefix branch gives BOTH prefix-causal
|
|
behaviour (a prefix q sees only earlier prefix tokens) AND full-prefix-visibility for
|
|
suffixes (every prefix index < every suffix index in a group, so kv <= q always holds
|
|
for a suffix q vs a prefix kv of its group), matching the SDPA oracle exactly.
|
|
"""
|
|
|
|
def mask_mod(b, h, q_idx, kv_idx):
|
|
same_group = group_of_kv[q_idx] == group_of_kv[kv_idx]
|
|
kv_is_prefix = is_prefix[kv_idx]
|
|
causal = kv_idx <= q_idx
|
|
same_suffix = (suffix_of_kv[kv_idx] == suffix_of_kv[q_idx]) & (~is_prefix[q_idx])
|
|
keep = same_group & ((kv_is_prefix & causal) | (same_suffix & causal))
|
|
return keep
|
|
|
|
return mask_mod
|
|
|
|
|
|
def get_block_mask(
|
|
seg: PrefixSegInfo,
|
|
device: torch.device,
|
|
compile_mask: bool = True,
|
|
) -> BlockMask:
|
|
"""Return a cached BlockMask for the segment signature (built once, reused).
|
|
|
|
CRITICAL: the block mask is cached and shared across BOTH the no-grad old/ref logprob
|
|
forward (which runs under torch.inference_mode) and the grad training forward. If the
|
|
mask were first built under inference_mode, its tensors would be INFERENCE tensors that
|
|
"cannot be saved for backward" when reused in the grad forward. We therefore build the
|
|
mask with inference mode explicitly DISABLED, so the same cached BlockMask is a normal
|
|
tensor usable by autograd. (The mask depends only on integer labels; it needs no grad.)
|
|
"""
|
|
key = (seg.signature, str(device))
|
|
bm = _BLOCK_MASK_CACHE.get(key)
|
|
if bm is not None:
|
|
return bm
|
|
|
|
# Move labels to the consumer (Q) device: with a sharded model the seg tensors live on
|
|
# input_ids.device and would index cross-device. Copies once per (signature, device).
|
|
# These copies must also run with inference mode DISABLED (same reason as the mask build):
|
|
# when this entry is first built under the no-grad old/ref forward's inference_mode and
|
|
# device != seg.device, a .to(device) copy would be an inference tensor that mask_mod
|
|
# captures, which then cannot be saved for backward when the grad training forward reuses
|
|
# the cached mask.
|
|
builder = _create_block_mask_compiled if compile_mask else create_block_mask
|
|
with torch.inference_mode(False):
|
|
mask_mod = _make_mask_mod(
|
|
seg.group_of_kv.to(device), seg.is_prefix.to(device), seg.suffix_of_kv.to(device)
|
|
)
|
|
bm = builder(
|
|
mask_mod,
|
|
B = 1,
|
|
H = None,
|
|
Q_LEN = seg.T_pad,
|
|
KV_LEN = seg.T_pad,
|
|
device = device,
|
|
)
|
|
# FIFO bound: GRPO lengths change nearly every step, so evict the oldest to cap GPU pins.
|
|
if len(_BLOCK_MASK_CACHE) >= 8:
|
|
_BLOCK_MASK_CACHE.pop(next(iter(_BLOCK_MASK_CACHE)))
|
|
_BLOCK_MASK_CACHE[key] = bm
|
|
return bm
|
|
|
|
|
|
def clear_block_mask_cache():
|
|
_BLOCK_MASK_CACHE.clear()
|
|
|
|
|
|
def _pad_qkv_seq(x: torch.Tensor, T_pad: int) -> torch.Tensor:
|
|
"""Zero-pad a [B, H, T, D] tensor along the sequence dim up to T_pad."""
|
|
T = x.shape[2]
|
|
if T_pad == T:
|
|
return x
|
|
pad = torch.zeros(x.shape[0], x.shape[1], T_pad - T, x.shape[3], device = x.device, dtype = x.dtype)
|
|
return torch.cat([x, pad], dim = 2)
|
|
|
|
|
|
def _run_flex(q, k, v, block_mask, enable_gqa, scale, compiled, T, T_pad):
|
|
"""Pad q/k/v to T_pad, run flex, slice the output back to T. q/k/v: [B,H,T,D]."""
|
|
qp = _pad_qkv_seq(q, T_pad)
|
|
kp = _pad_qkv_seq(k, T_pad)
|
|
vp = _pad_qkv_seq(v, T_pad)
|
|
if compiled:
|
|
out = _flex_attention_compiled(
|
|
qp,
|
|
kp,
|
|
vp,
|
|
block_mask = block_mask,
|
|
enable_gqa = enable_gqa,
|
|
scale = scale,
|
|
kernel_options = _kernel_options_for_dtype(qp.dtype),
|
|
)
|
|
else:
|
|
# eager path (fp64 parity): dense scores, no kernel_options.
|
|
out = flex_attention(
|
|
qp,
|
|
kp,
|
|
vp,
|
|
block_mask = block_mask,
|
|
enable_gqa = enable_gqa,
|
|
scale = scale,
|
|
)
|
|
return out[:, :, :T, :]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The kernel entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def flex_shared_prefix_attention(
|
|
Q: torch.Tensor,
|
|
K: torch.Tensor,
|
|
V: torch.Tensor,
|
|
prefix_seg_info: PrefixSegInfo,
|
|
scale: Optional[float] = None,
|
|
block_mask: Optional[BlockMask] = None,
|
|
compiled: bool = True,
|
|
) -> torch.Tensor:
|
|
"""Shared-prefix attention via FlexAttention.
|
|
|
|
Parameters
|
|
----------
|
|
Q, K, V : Tensor [1, T, n_heads, head_dim]
|
|
(Q has n_heads, K/V have n_kv_heads for GQA).
|
|
prefix_seg_info : PrefixSegInfo
|
|
scale : optional float, softmax scale (defaults to 1/sqrt(head_dim)).
|
|
block_mask : optional precomputed BlockMask (else built/cached from seg info).
|
|
|
|
Returns
|
|
-------
|
|
Tensor [1, T, n_heads, head_dim], identical semantics to the SDPA oracle branch.
|
|
"""
|
|
assert Q.dim() == 4 and Q.shape[0] == 1, f"expected [1,T,H,D], got {tuple(Q.shape)}"
|
|
device = Q.device
|
|
# FlexAttention wants [B, H, T, D].
|
|
q = Q.transpose(1, 2) # [1, n_heads, T, D]
|
|
k = K.transpose(1, 2) # [1, n_kv_heads, T, D]
|
|
v = V.transpose(1, 2)
|
|
|
|
n_heads = q.shape[1]
|
|
n_kv = k.shape[1]
|
|
enable_gqa = n_heads != n_kv
|
|
T = q.shape[2]
|
|
T_pad = prefix_seg_info.T_pad
|
|
assert T == prefix_seg_info.T, f"Q length {T} != seg.T {prefix_seg_info.T}"
|
|
|
|
if block_mask is None:
|
|
block_mask = get_block_mask(prefix_seg_info, device, compile_mask = compiled)
|
|
|
|
out = _run_flex(q, k, v, block_mask, enable_gqa, scale, compiled, T, T_pad)
|
|
# back to [1, T, n_heads, D]
|
|
return out.transpose(1, 2).contiguous()
|
|
|
|
|
|
__all__ = [
|
|
"PrefixSegInfo",
|
|
"build_seg_info_multigroup",
|
|
"build_seg_info_from_layout",
|
|
"get_block_mask",
|
|
"clear_block_mask_cache",
|
|
"flex_shared_prefix_attention",
|
|
]
|