RuVI-AI
/

File size: 6,542 Bytes
05acbb5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d55c0ec
05acbb5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Attention kernel with two backends, chosen once at import.

* FlexAttention (default): CUDA with compute capability >= 8.0 (sm80+), torch >= 2.5.
  Masks are BlockMasks; the mask logic lives in mask_mod.
* xFormers memory-efficient attention: CUDA GPUs below sm80 (no Triton/Flex support).
  Masks are dense boolean (T, T) or (B, T, T) tensors converted to additive biases.

Every attention layer calls `fused_attention(q, k, v, mask)`; the mask object comes from
`causal_masks` or `doc_masks`, so the backend is invisible to model.py.

Attention-weight dropout is not supported on either backend (DenseMHA refuses dropout > 0).
"""
import functools
import importlib
import warnings

import torch
import torch.nn.functional as F
from torch.nn.attention.flex_attention import create_block_mask, flex_attention

_IS_CUDA = torch.cuda.is_available() and torch.version.cuda is not None
_USE_XFORMERS = _IS_CUDA and torch.cuda.get_device_capability() < (8, 0)

_XFORMERS_OK = _USE_XFORMERS  # cleared on the first failure; then SDPA is used

if _USE_XFORMERS:
    # Loaded dynamically: Transformers scans source files for static imports and would
    # require xformers on every machine, including ones that never use it.
    xops = importlib.import_module("xformers.ops")  # pip install xformers (match your torch)
    _flex = None
else:
    xops = None
    _flex = torch.compile(flex_attention, dynamic=False) if _IS_CUDA else flex_attention


class _XMask:
    """Dense allowed-positions mask for the xFormers path. allowed: bool (T, T) or (B, T, T)."""
    __slots__ = ("allowed",)

    def __init__(self, allowed: torch.Tensor):
        self.allowed = allowed


def fused_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, mask) -> torch.Tensor:
    """q: (B, Hq, T, D); k, v: (B, Hkv, T, D). GQA is handled natively (Hq a multiple of Hkv)."""
    if isinstance(mask, _XMask):
        return _xformers_attention(q, k, v, mask.allowed)
    return _flex(q, k, v, block_mask=mask, enable_gqa=q.shape[1] != k.shape[1])


def _xformers_attention(q, k, v, allowed):
    B, Hq, T, D = q.shape
    Hkv = k.shape[1]
    if Hq != Hkv:
        # query head h uses kv head h // group, the same grouping FlexAttention's enable_gqa uses
        group = Hq // Hkv
        k = k.repeat_interleave(group, dim=1)
        v = v.repeat_interleave(group, dim=1)
    bias = torch.zeros(allowed.shape, dtype=q.dtype, device=q.device).masked_fill(~allowed, float("-inf"))
    bias = bias[None, None] if bias.dim() == 2 else bias[:, None]
    bias = bias.expand(B, Hq, T, T)
    global _XFORMERS_OK
    if _XFORMERS_OK:
        try:
            # xFormers expects (B, T, H, D)
            out = xops.memory_efficient_attention(
                q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), attn_bias=bias)
            _record_backend("xformers")
            return out.transpose(1, 2)
        except Exception as e:
            # Typical cause: xformers built for a different torch/CUDA than the runtime.
            _XFORMERS_OK = False
            warnings.warn(f"xformers memory_efficient_attention unavailable ({type(e).__name__}: {e}); "
                          "using torch scaled_dot_product_attention with the same mask.")
    # Same math with PyTorch's built-in attention (works on any CUDA GPU, no extra package).
    out = F.scaled_dot_product_attention(q, k, v, attn_mask=bias)
    _record_backend("sdpa_masked")
    return out


# ---------------------------------------------------------------------------
# Masks. One rule for every layer: window = window_size or seq_len; since q - kv <= seq_len - 1,
# "no window" is the same expression as "window = seq_len".
# ---------------------------------------------------------------------------

@functools.lru_cache(maxsize=None)
def _causal_mask(seq_len: int, window_size, device_key: str):
    window = window_size or seq_len

    def mask_mod(b, h, q_idx, kv_idx):
        return (kv_idx <= q_idx) & (q_idx - kv_idx < window)

    return create_block_mask(mask_mod, B=None, H=None, Q_LEN=seq_len, KV_LEN=seq_len, device=device_key)


@functools.lru_cache(maxsize=None)
def _xcausal_mask(seq_len: int, window_size, device_key: str):
    idx = torch.arange(seq_len, device=device_key)
    q_idx = idx[:, None]
    kv_idx = idx[None, :]
    window = window_size or seq_len
    return _XMask((kv_idx <= q_idx) & (q_idx - kv_idx < window))


@torch.compiler.disable
def causal_masks(seq_len: int, windows, device) -> dict:
    """{window_size: mask} for plain causal attention; cached per (seq_len, window, device)."""
    if _USE_XFORMERS:
        return {w: _xcausal_mask(seq_len, w, str(device)) for w in windows}
    return {w: _causal_mask(seq_len, w, str(device)) for w in windows}


@torch.compiler.disable
def doc_masks(doc_ids: torch.Tensor, windows) -> dict:
    """{window_size: mask} for causal + same-document attention. doc_ids: (B, T) int, per batch."""
    b_, t_ = doc_ids.shape

    if _USE_XFORMERS:
        idx = torch.arange(t_, device=doc_ids.device)
        q_idx = idx[:, None]
        kv_idx = idx[None, :]
        same_doc = doc_ids[:, :, None] == doc_ids[:, None, :]          # (B, T, T)

        def build_x(window_size):
            window = window_size or t_
            return _XMask(same_doc & (kv_idx <= q_idx) & (q_idx - kv_idx < window))

        return {w: build_x(w) for w in windows}

    def build(window_size):
        window = window_size or t_

        def mask_mod(b, h, q_idx, kv_idx):
            return (doc_ids[b, q_idx] == doc_ids[b, kv_idx]) & (kv_idx <= q_idx) & (q_idx - kv_idx < window)

        return create_block_mask(mask_mod, B=b_, H=None, Q_LEN=t_, KV_LEN=t_,
                                 device=doc_ids.device, _compile=True)

    return {w: build(w) for w in windows}


# ---------------------------------------------------------------------------
# Telemetry counters (model.py records its Liger/Triton success/fallback events here).
# ---------------------------------------------------------------------------

_backend_stats: dict[str, int] = {}


def _record_backend(name: str) -> None:
    # Inside a compiled region this body runs only at trace time, so counting here under-reports.
    # Record nothing while compiling rather than report wrong numbers.
    if torch.compiler.is_compiling():
        return
    _backend_stats[name] = _backend_stats.get(name, 0) + 1


def get_backend_stats() -> dict[str, int]:
    return dict(_backend_stats)


def reset_backend_stats() -> None:
    _backend_stats.clear()