stream-cqsa 2.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. stream_cqsa/__init__.py +90 -0
  2. stream_cqsa/adapters.py +127 -0
  3. stream_cqsa/api.py +363 -0
  4. stream_cqsa/attention_kernel/Custom.py +97 -0
  5. stream_cqsa/attention_kernel/FA.py +92 -0
  6. stream_cqsa/attention_kernel/__init__.py +7 -0
  7. stream_cqsa/autoconfig.py +607 -0
  8. stream_cqsa/autograd_op.py +596 -0
  9. stream_cqsa/backends/__init__.py +2 -0
  10. stream_cqsa/backends/exact/__init__.py +25 -0
  11. stream_cqsa/backends/exact/online_softmax.py +279 -0
  12. stream_cqsa/backends/exact/rectangular_ooc_backend.py +68 -0
  13. stream_cqsa/backends/exact/rectangular_ooc_inner_kernel.py +89 -0
  14. stream_cqsa/backends/exact/sdpa_backend.py +88 -0
  15. stream_cqsa/backends/exact/sdpa_inner_kernel.py +573 -0
  16. stream_cqsa/baselines/__init__.py +2 -0
  17. stream_cqsa/baselines/longnet/__init__.py +21 -0
  18. stream_cqsa/baselines/longnet/longnet_backend.py +100 -0
  19. stream_cqsa/baselines/longnet/longnet_inner_kernel.py +294 -0
  20. stream_cqsa/baselines/longnet/longnet_mask.py +244 -0
  21. stream_cqsa/config.py +19 -0
  22. stream_cqsa/cqs_attention.py +158 -0
  23. stream_cqsa/cqs_attention_stream.py +1949 -0
  24. stream_cqsa/cqs_attention_wrapper.py +760 -0
  25. stream_cqsa/cqs_mask.py +647 -0
  26. stream_cqsa/cqsa_probe_capacity.py +534 -0
  27. stream_cqsa/devkit.py +413 -0
  28. stream_cqsa/distributed.py +181 -0
  29. stream_cqsa/doctor.py +125 -0
  30. stream_cqsa/interface.py +444 -0
  31. stream_cqsa/memory_fitting.py +498 -0
  32. stream_cqsa/native_autograd.py +211 -0
  33. stream_cqsa/native_wave.py +569 -0
  34. stream_cqsa/offload.py +105 -0
  35. stream_cqsa/oom_fallback.py +346 -0
  36. stream_cqsa/progress.py +129 -0
  37. stream_cqsa/reference.py +707 -0
  38. stream_cqsa/stable_stream.py +2521 -0
  39. stream_cqsa/streamed_fwd_bwd.py +2169 -0
  40. stream_cqsa/triton_kernel.py +594 -0
  41. stream_cqsa-2.1.2.dist-info/METADATA +22 -0
  42. stream_cqsa-2.1.2.dist-info/RECORD +46 -0
  43. stream_cqsa-2.1.2.dist-info/WHEEL +5 -0
  44. stream_cqsa-2.1.2.dist-info/entry_points.txt +2 -0
  45. stream_cqsa-2.1.2.dist-info/licenses/LICENSE +29 -0
  46. stream_cqsa-2.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,90 @@
1
+ __version__ = "2.1.2"
2
+
3
+ from .stable_stream import (
4
+ stream_cqsa_forward,
5
+ stream_cqsa_backward,
6
+ TraceRecorder,
7
+ )
8
+ from .oom_fallback import (stream_cqsa_auto, attention_oom_safe,
9
+ ESCALATION, ESCALATION_FAST)
10
+ from .native_autograd import stream_cqsa_attn, StreamCQSAAttention
11
+ from .api import attention, estimate, patch_sdpa, unpatch_sdpa, patched_sdpa, kernels_available
12
+ from .doctor import doctor
13
+ from .progress import verbose_enabled
14
+
15
+ from .interface import (
16
+ flash_attn_func,
17
+ flash_attn_func_cqs,
18
+ flash_attn_func_cqs_group_bits,
19
+ flash_attn_bwd_cqs_group_bits,
20
+ flash_attn_func_cqsa,
21
+ )
22
+ from .config import (
23
+ DEFAULT_PROBING_HISTORY,
24
+ DEFAULT_PROBING_HISTORY_DIR,
25
+ GLOBAL_STREAM_SEED,
26
+ MEM_BUDGET_COE,
27
+ )
28
+ from .cqsa_probe_capacity import CQSAProbe
29
+ from .cqs_attention_stream import (
30
+ CQSAStreamRunner,
31
+ SubsequenceAttentionFn,
32
+ compare_O,
33
+ run_scqsa_stream,
34
+ stream_cqsa,
35
+ )
36
+ from .attention_kernel import (
37
+ default_subsequence_attention,
38
+ custom_attn,
39
+ )
40
+ from .cqs_mask import CQS_mask
41
+ from .memory_fitting import fitting_mem, pred_mem, best_seq_length, estimate_memory_time_from_model
42
+ from .autograd_op import stream_cqsa_autograd
43
+
44
+ __all__ = [
45
+ # --- the one-call API -------------------------------------------------
46
+ "attention", # SDPA signature; monolithic when it fits, exact decomposition when it does not
47
+ "estimate", # dry run: what a call would cost here
48
+ "patch_sdpa", "unpatch_sdpa", "patched_sdpa",
49
+ "doctor", "kernels_available",
50
+ # --- primary entry points -------------------------------------------
51
+ "stream_cqsa_auto", # "just run it": escalates until it fits
52
+ "stream_cqsa_forward", # explicit forward, returns (out, lse)
53
+ "stream_cqsa_backward", # explicit backward, needs the global lse
54
+ "stream_cqsa_attn", # autograd-aware; supports .backward()
55
+ "StreamCQSAAttention", # the same, as an nn.Module
56
+ "attention_oom_safe",
57
+ "ESCALATION",
58
+ "ESCALATION_FAST",
59
+ "TraceRecorder",
60
+ # --- everything else --------------------------------------------------
61
+ "flash_attn_func",
62
+ "flash_attn_func_cqs",
63
+ "flash_attn_func_cqs_group_bits",
64
+ "flash_attn_bwd_cqs_group_bits",
65
+ "flash_attn_func_cqsa",
66
+ "CQSAProbe",
67
+ "CQSAStreamRunner",
68
+ "SubsequenceAttentionFn",
69
+ "DEFAULT_PROBING_HISTORY",
70
+ "DEFAULT_PROBING_HISTORY_DIR",
71
+ "GLOBAL_STREAM_SEED",
72
+ "MEM_BUDGET_COE",
73
+ "run_scqsa_stream",
74
+ "stream_cqsa",
75
+ "compare_O",
76
+ "default_subsequence_attention",
77
+ "custom_attn",
78
+ "CQS_mask",
79
+ "fitting_mem",
80
+ "pred_mem",
81
+ "best_seq_length",
82
+ "estimate_memory_time_from_model",
83
+ "stream_cqsa_autograd",
84
+ ]
85
+
86
+ # v2 additions
87
+ from .autoconfig import (detect_hardware, hardware_from_dict, plan, calibrate, autotune, # noqa: E402
88
+ auto_attention, QUORUM_SETS, CostModel, Plan)
89
+ from .devkit import compare_kernels, quick_bench, Config, run_config # noqa: E402
90
+ from .adapters import flex_inner, dense_inner # noqa: E402
@@ -0,0 +1,127 @@
1
+ """
2
+ Adapters that turn a monolithic attention implementation into a Stream-CQSA
3
+ inner kernel without writing CUDA.
4
+
5
+ What an inner kernel must provide (see devkit.compare_kernels):
6
+
7
+ inner(q_i, k_i, v_i, group_bits, *, causal, scale, **_) -> (out_i, lse_i)
8
+
9
+ i.e. attention restricted to the pair set {(r, c): bits[r] & bits[c] == 0,
10
+ (c <= r if causal)}, returning the normalised output ``out_i [B, L, H, D]``
11
+ AND the per-row log-sum-exp of the kept scores ``lse_i [B, L, H]`` (both
12
+ token-major, float32). Any implementation that (a) accepts an
13
+ arbitrary pairwise mask and (b) returns the log-sum-exp can therefore be
14
+ plugged in mechanically. Two such families exist in PyTorch today:
15
+
16
+ * ``flex_inner(score_mod=None)`` -- torch's FlexAttention. The CQS pair set is
17
+ expressed as a ``mask_mod`` (a block mask is built per subproblem, so the
18
+ masked tiles are skipped by the compiled kernel too), and ``return_lse=True``
19
+ gives the lse. Any attention that FlexAttention can express -- ALiBi,
20
+ soft-capping, sliding windows, document masks, relative biases -- becomes a
21
+ Stream-CQSA inner kernel by passing its ``score_mod``/extra mask, with no
22
+ other change. That is the "automatic conversion" for the score-modification
23
+ family.
24
+
25
+ * ``dense_inner(mono_masked)`` -- for small subproblems, any callable that
26
+ takes a dense boolean mask and returns (out, lse) (or just out, in which
27
+ case the lse is recomputed densely). This is the differential-testing route.
28
+
29
+ Kernels that return NO log-sum-exp cannot be adapted losslessly: the merge of
30
+ subproblems needs the row statistics, and recovering them from the output
31
+ alone is impossible (the output is normalised). For those the only exact
32
+ option is a second pass that computes the lse (``lse_from_scores``), which
33
+ costs another QK^T; the devkit reports that overhead honestly.
34
+ """
35
+ from __future__ import annotations
36
+
37
+ from typing import Callable
38
+
39
+ import torch
40
+
41
+ _flex_cache: dict = {}
42
+
43
+
44
+ def _bits_pair_mask(bits: torch.Tensor, causal: bool):
45
+ def mask_mod(b, h, q_idx, kv_idx):
46
+ keep = (bits[q_idx] & bits[kv_idx]) == 0
47
+ if causal:
48
+ keep = keep & (kv_idx <= q_idx)
49
+ return keep
50
+ return mask_mod
51
+
52
+
53
+ def flex_inner(score_mod: Callable | None = None, *, extra_mask_mod: Callable | None = None,
54
+ block_size: int = 128, compile: bool = True, global_positions: bool = True) -> Callable:
55
+ """
56
+ Build a Stream-CQSA inner kernel from torch FlexAttention.
57
+
58
+ `score_mod(score, b, h, q_idx, kv_idx)` and `extra_mask_mod(b, h, q_idx, kv_idx)`
59
+ are written against GLOBAL token positions (as in the monolithic call): a
60
+ subproblem sees a gathered subsequence, so the engine hands the inner kernel
61
+ the gather index (`token_ids`) and the adapter remaps local indices through
62
+ it before calling the user's functions. That is the one thing an automatic
63
+ conversion must get right for position-dependent attention (ALiBi, sliding
64
+ windows, document masks); with `global_positions=False` the raw local
65
+ indices are passed instead (only correct for position-free score_mods).
66
+ """
67
+ from torch.nn.attention.flex_attention import flex_attention, create_block_mask
68
+ fa = torch.compile(flex_attention, dynamic=False) if compile else flex_attention
69
+
70
+ def inner(q_i, k_i, v_i, group_bits, *, causal: bool, scale: float, token_ids=None, **_):
71
+ B, L, H, D = q_i.shape
72
+ dev = q_i.device
73
+ bits = group_bits.to(dev, torch.int64)
74
+ mm = _bits_pair_mask(bits, causal)
75
+ ids = token_ids.to(dev, torch.int64) if (token_ids is not None and global_positions) else None
76
+ if extra_mask_mod is not None:
77
+ base = mm
78
+ if ids is not None:
79
+ mm = lambda b, h, qi, ki: base(b, h, qi, ki) & extra_mask_mod(b, h, ids[qi], ids[ki])
80
+ else:
81
+ mm = lambda b, h, qi, ki: base(b, h, qi, ki) & extra_mask_mod(b, h, qi, ki)
82
+ sm = score_mod
83
+ if score_mod is not None and ids is not None:
84
+ sm = lambda score, b, h, qi, ki: score_mod(score, b, h, ids[qi], ids[ki])
85
+ bm = create_block_mask(mm, B=None, H=None, Q_LEN=L, KV_LEN=L, device=dev, BLOCK_SIZE=block_size)
86
+ qb, kb, vb = (t.transpose(1, 2) for t in (q_i, k_i, v_i)) # [B,H,L,D]
87
+ out, lse = fa(qb, kb, vb, score_mod=sm, block_mask=bm, scale=float(scale), return_lse=True)
88
+ # rows with no kept pair: flex returns lse=-inf and out=0, which is the contract.
89
+ # Engine contract: out_i [B,L,H,D], lse_i [B,L,H] (token-major, like the CUDA kernel's return).
90
+ return out.transpose(1, 2).float(), lse.float().transpose(1, 2)
91
+
92
+ return inner
93
+
94
+
95
+ def lse_from_scores(q_i, k_i, v_i, keep: torch.Tensor, scale: float) -> torch.Tensor:
96
+ """Dense fp32 per-row lse over kept pairs (second pass for kernels without lse). [B,H,L]"""
97
+ s = torch.matmul(q_i.transpose(1, 2).float(), k_i.transpose(1, 2).float().transpose(-1, -2)) * float(scale)
98
+ s = s.masked_fill(~keep[None, None], float("-inf"))
99
+ return torch.logsumexp(s, dim=-1)
100
+
101
+
102
+ def dense_inner(mono_masked: Callable, *, returns_lse: bool = True) -> Callable:
103
+ """
104
+ Wrap ``mono_masked(q, k, v, mask, scale) -> (out [B,H,L,D], lse [B,H,L])`` (or
105
+ just out if returns_lse=False) where mask is a dense bool [L, L] (True = keep).
106
+ Only for small L (the mask is O(L^2)).
107
+ """
108
+ def inner(q_i, k_i, v_i, group_bits, *, causal: bool, scale: float, **_):
109
+ L = q_i.shape[1]
110
+ bits = group_bits.to(q_i.device, torch.int64)
111
+ keep = (bits[:, None] & bits[None, :]) == 0
112
+ if causal:
113
+ keep = keep & torch.ones(L, L, dtype=torch.bool, device=q_i.device).tril()
114
+ qb, kb, vb = (t.transpose(1, 2) for t in (q_i, k_i, v_i))
115
+ r = mono_masked(qb, kb, vb, keep, float(scale))
116
+ if returns_lse:
117
+ out, lse = r
118
+ else:
119
+ out, lse = r, lse_from_scores(q_i, k_i, v_i, keep, scale)
120
+ return out.transpose(1, 2).float(), lse.float().transpose(1, 2) # [B,L,H,D], [B,L,H]
121
+ return inner
122
+
123
+
124
+ def sdpa_masked(qb, kb, vb, keep, scale):
125
+ """Reference monolithic-with-mask kernel (torch SDPA, no lse): use with dense_inner(..., returns_lse=False)."""
126
+ import torch.nn.functional as F
127
+ return F.scaled_dot_product_attention(qb, kb, vb, attn_mask=keep, scale=scale)
stream_cqsa/api.py ADDED
@@ -0,0 +1,363 @@
1
+ """
2
+ The one entry point: ``stream_cqsa.attention`` with the signature of
3
+ ``torch.nn.functional.scaled_dot_product_attention``.
4
+
5
+ out = stream_cqsa.attention(q, k, v, is_causal=True)
6
+
7
+ * below the memory boundary it IS the monolithic call (SDPA / FlashAttention);
8
+ * above it, the planner picks the decomposition (depth, quorum set, accumulator
9
+ placement, host streaming, concurrency) from the free memory and the call
10
+ runs exactly, on the best kernel available (native wave kernel, CUDA
11
+ extension, or the Triton kernels when nothing is compiled);
12
+ * gradients flow when the inputs require them;
13
+ * ``verbose=True`` (or ``CQSA_VERBOSE=1``) shows what is happening.
14
+
15
+ ``patch_sdpa()`` swaps this in for ``F.scaled_dot_product_attention`` so an
16
+ existing model runs unchanged; ``estimate(N, ...)`` is the dry run that says
17
+ what a call would cost before it is made.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import contextlib
22
+ import os
23
+ import warnings
24
+ from typing import Any, Optional
25
+
26
+ import torch
27
+ import torch.nn.functional as F
28
+
29
+ from .progress import verbose_enabled
30
+
31
+ __all__ = ["attention", "estimate", "patch_sdpa", "unpatch_sdpa", "patched_sdpa", "kernels_available"]
32
+
33
+
34
+ def kernels_available() -> dict[str, bool]:
35
+ """Which attention kernels this environment can run."""
36
+ from . import interface as I
37
+ out = {"cuda_extension": I.cqsa_cuda is not None, "cuda_extension_noncausal": I.cqsa_cuda_noncausal is not None}
38
+ try:
39
+ from .native_wave import native_available
40
+ out["native_wave"] = native_available()
41
+ except Exception:
42
+ out["native_wave"] = False
43
+ try:
44
+ import triton # noqa: F401
45
+ from . import triton_kernel # noqa: F401
46
+ out["triton"] = True
47
+ except Exception:
48
+ out["triton"] = False
49
+ try:
50
+ import flash_attn # noqa: F401
51
+ out["flash_attn"] = True
52
+ except Exception:
53
+ out["flash_attn"] = False
54
+ return out
55
+
56
+
57
+ def _check_inputs(q, k, v, attn_mask, dropout_p, enable_gqa):
58
+ if q.dim() != 4 or k.dim() != 4 or v.dim() != 4:
59
+ raise ValueError(f"stream_cqsa.attention expects q/k/v as [B, H, N, D]; got {tuple(q.shape)}, {tuple(k.shape)}, {tuple(v.shape)}")
60
+ if attn_mask is not None:
61
+ raise NotImplementedError("stream_cqsa.attention supports the plain and the causal (is_causal=True) cases; an explicit "
62
+ "attn_mask is not supported. For FlexAttention-style masks use stream_cqsa.adapters.flex_inner.")
63
+ if dropout_p:
64
+ raise NotImplementedError("stream_cqsa.attention does not support dropout (dropout_p must be 0).")
65
+ if enable_gqa or k.shape[1] != q.shape[1]:
66
+ raise NotImplementedError("stream_cqsa.attention needs the same number of heads for q and k/v (no GQA); repeat k/v heads first.")
67
+ if q.shape != k.shape or q.shape != v.shape:
68
+ raise ValueError("q, k and v must have the same shape [B, H, N, D] (cross-attention with N_q != N_k is not supported)")
69
+ if q.dtype not in (torch.float16, torch.bfloat16):
70
+ raise TypeError(f"stream_cqsa.attention needs fp16 or bf16 inputs (got {q.dtype}); cast with .half() or .bfloat16(). "
71
+ "Accumulation is fp32 internally regardless.")
72
+ if not (q.dtype == k.dtype == v.dtype):
73
+ raise TypeError("q, k and v must share a dtype")
74
+ if not (q.device == k.device == v.device):
75
+ raise ValueError("q, k and v must be on the same device")
76
+
77
+
78
+ def _is_oom(exc: BaseException) -> bool:
79
+ return isinstance(exc, torch.cuda.OutOfMemoryError) or "out of memory" in str(exc).lower()
80
+
81
+
82
+ def attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, attn_mask=None, dropout_p: float = 0.0,
83
+ is_causal: bool = False, scale: Optional[float] = None, enable_gqa: bool = False, *,
84
+ verbose: Optional[bool] = None, hardware=None, kernel: str = "auto", plan_only: bool = False,
85
+ return_plan: bool = False, **overrides) -> torch.Tensor:
86
+ """
87
+ Exact attention that always fits. Drop-in for ``F.scaled_dot_product_attention``
88
+ (same positional signature; masks, dropout and GQA are rejected with a message).
89
+
90
+ q/k/v ``[B, H, N, D]`` fp16/bf16, on a device or in host memory. Returns the
91
+ output in the inputs' dtype and device. Differentiable when the inputs require
92
+ gradients. ``kernel``: "auto" (native wave kernel when built and applicable,
93
+ else the CUDA extension, else Triton), "wave", "cuda", "triton".
94
+ ``overrides`` (itr=, c=, interest_set=, max_parallel=, ...) pin engine settings.
95
+ ``plan_only=True`` returns the plan without computing anything.
96
+ """
97
+ _INSIDE[0] += 1
98
+ try:
99
+ return _attention(q, k, v, attn_mask, dropout_p, is_causal, scale, enable_gqa, verbose=verbose, hardware=hardware,
100
+ kernel=kernel, plan_only=plan_only, return_plan=return_plan, **overrides)
101
+ finally:
102
+ _INSIDE[0] -= 1
103
+
104
+
105
+ def _attention(q, k, v, attn_mask, dropout_p, is_causal, scale, enable_gqa, *, verbose, hardware, kernel, plan_only,
106
+ return_plan, **overrides):
107
+ from .autoconfig import plan as _plan, hardware_from_dict, detect_hardware
108
+ _check_inputs(q, k, v, attn_mask, dropout_p, enable_gqa)
109
+ B, H, N, D = q.shape
110
+ if scale is None:
111
+ scale = float(D) ** -0.5
112
+ on_cuda = q.device.type == "cuda"
113
+ out_device = q.device
114
+ hw = hardware_from_dict(hardware) if isinstance(hardware, dict) else (hardware or detect_hardware())
115
+ direction = "bwd" if torch.is_grad_enabled() and (q.requires_grad or k.requires_grad or v.requires_grad) else "fwd"
116
+ p = _plan(N=N, B=B, H=H, D=D, dtype=q.dtype, causal=bool(is_causal), hardware=hw, direction=direction,
117
+ allow_distributed=False)
118
+ if plan_only:
119
+ return p
120
+ vb = verbose_enabled(verbose)
121
+
122
+ # ---- monolithic: the call fits, so it IS the normal kernel ------------------
123
+ if p.mode == "mono" and not overrides:
124
+ if vb:
125
+ print(f"Stream-CQSA: N={N} fits ({p.est_peak_gib:.1f} GiB) -- monolithic call, no decomposition", flush=True)
126
+ qd, kd, vd = ((t if on_cuda else t.to("cuda", non_blocking=True)) for t in (q, k, v))
127
+ try:
128
+ out = _TRUE_SDPA(qd, kd, vd, is_causal=bool(is_causal), scale=scale)
129
+ out = out if on_cuda else out.to(q.device)
130
+ return (out, p) if return_plan else out
131
+ except Exception as exc: # noqa: BLE001
132
+ if not _is_oom(exc):
133
+ raise
134
+ if vb:
135
+ print("Stream-CQSA: the monolithic call ran out of memory after all; decomposing", flush=True)
136
+ del qd, kd, vd
137
+ torch.cuda.empty_cache()
138
+ p = _plan(N=N, B=B, H=H, D=D, dtype=q.dtype, causal=bool(is_causal), hardware=detect_hardware(),
139
+ direction=direction, allow_distributed=False)
140
+ if p.mode == "mono": # the planner still thinks it fits: force a decomposition
141
+ p.mode, p.itr, p.c, p.interest_set = "cqsa", 1, 7, (0, 1, 3)
142
+
143
+ # ---- decomposed ------------------------------------------------------------
144
+ kw = p.engine_kwargs() if p.mode != "mono" else dict(itr=1, c=7, interest_set=(0, 1, 3))
145
+ if on_cuda and kw.get("stream_from_host"):
146
+ # The inputs are on the device. Prefer the fastest configuration that leaves them
147
+ # there; stream from the host only if nothing device-resident fits (then a pinned
148
+ # host copy is made -- the caller's device tensors stay where they are).
149
+ dev_ok = [c for c in p.candidates if c["ok"] and c["mode"] == "cqsa" and not c["stream_from_host"]]
150
+ if dev_ok:
151
+ c0 = min(dev_ok, key=lambda c: c["time"])
152
+ kw = dict(itr=int(c0["itr"]), c=int(c0["c"]), interest_set=tuple(c0["interest_set"]),
153
+ low_memory=(c0["acc"] == "cpu"), accumulate_on_gpu=(c0["acc"] == "gpu"),
154
+ stream_from_host=False, max_parallel=int(c0["n_par"]), shared_chunks=False)
155
+ elif direction == "bwd":
156
+ # Copying the inputs to the host would sever the autograd graph. Keep them on the
157
+ # device, park the accumulator in host memory, and let the engine escalate the
158
+ # depth on its own if the device is still short.
159
+ if vb:
160
+ print("Stream-CQSA: no device-resident configuration fits the estimate; keeping Q/K/V on the device with a "
161
+ "host accumulator and escalating on demand", flush=True)
162
+ kw.update(stream_from_host=False, low_memory=True, accumulate_on_gpu=False, shared_chunks=False)
163
+ else:
164
+ if vb:
165
+ print("Stream-CQSA: no device-resident configuration fits; copying Q/K/V to pinned host memory and streaming", flush=True)
166
+ if direction == "bwd":
167
+ q, k, v = (t.to("cpu") for t in (q, k, v)) # differentiable copies: the graph stays intact
168
+ else:
169
+ q, k, v = (t.detach().to("cpu").pin_memory() for t in (q, k, v))
170
+ on_cuda = False
171
+ kw.update(overrides)
172
+ if not kw.get("stream_from_host"):
173
+ kw["shared_chunks"] = False
174
+ use_wave = _pick_wave(kernel, kw, is_causal, q, direction)
175
+ if vb:
176
+ print(f"Stream-CQSA: {p.reason}", flush=True)
177
+ if use_wave:
178
+ from .native_wave import wave_attention, wave_forward
179
+ wkw = dict(itr=int(kw.get("itr", 1)), c=int(kw.get("c", 7)), interest_set=tuple(kw.get("interest_set", (0, 1, 3))))
180
+ try:
181
+ if direction == "bwd":
182
+ out = wave_attention(q, k, v, causal=bool(is_causal), scale=scale, verbose=verbose, **wkw)
183
+ else:
184
+ out, _ = wave_forward(q, k, v, causal=bool(is_causal), scale=scale, verbose=verbose, **wkw)
185
+ except Exception as exc: # noqa: BLE001
186
+ if not _is_oom(exc) or kernel == "wave":
187
+ raise
188
+ # the wave engine keeps its accumulator on the device; the classic engine can offload it
189
+ if vb:
190
+ print("Stream-CQSA: the wave engine ran out of memory; retrying on the streaming engine", flush=True)
191
+ torch.cuda.empty_cache()
192
+ use_wave = False
193
+ if not use_wave:
194
+ from .native_autograd import stream_cqsa_attn
195
+ from .stable_stream import stream_cqsa_forward
196
+ if kernel == "triton":
197
+ os.environ["CQSA_FORWARD"] = "triton"; os.environ["CQSA_BACKWARD"] = "triton"
198
+ host = not on_cuda
199
+ if direction == "bwd":
200
+ out = stream_cqsa_attn(q, k, v, causal=bool(is_causal), scale=scale, verbose=verbose,
201
+ itr=kw.get("itr", "auto"), c=kw.get("c", 7), interest_set=kw.get("interest_set", (0, 1, 3)),
202
+ stream_from_host=bool(kw.get("stream_from_host", host)),
203
+ accumulate_on_gpu=bool(kw.get("accumulate_on_gpu", True)),
204
+ max_parallel=kw.get("max_parallel"))
205
+ else:
206
+ kw.setdefault("stream_from_host", host)
207
+ # leave the fp32 result where the accumulator is; _deliver casts and moves it
208
+ out, _ = stream_cqsa_forward(q, k, v, causal=bool(is_causal), scale=scale, verbose=verbose, out_device="acc", **kw)
209
+ out = _deliver(out, out_device, q.dtype, vb)
210
+ return (out, p) if return_plan else out
211
+
212
+
213
+ def _deliver(out: torch.Tensor, device, dtype, vb: bool) -> torch.Tensor:
214
+ """Hand the result over in the caller's dtype and on the caller's device.
215
+
216
+ The engines return fp32; the cast needs room for a second copy. When the device
217
+ cannot take it, the cast is done in host memory and the result moved back; if
218
+ even the half-size result does not fit next to what the caller keeps on the
219
+ device, it is returned in host memory with a warning rather than failing after
220
+ the work is done."""
221
+ if out.dtype == dtype and out.device == device:
222
+ return out
223
+ try:
224
+ return out.to(device=device, dtype=dtype)
225
+ except Exception as exc: # noqa: BLE001
226
+ if not _is_oom(exc):
227
+ raise
228
+ host = out.to("cpu") if out.device.type != "cpu" else out
229
+ del out
230
+ torch.cuda.empty_cache()
231
+ host = host.to(dtype)
232
+ if device.type == "cpu":
233
+ return host
234
+ try:
235
+ return host.to(device)
236
+ except Exception as exc: # noqa: BLE001
237
+ if not _is_oom(exc):
238
+ raise
239
+ warnings.warn(f"Stream-CQSA: the result ({host.numel() * host.element_size() / 2**30:.1f} GiB) does not fit on {device} "
240
+ f"next to what is already there; returning it in host memory.", RuntimeWarning, stacklevel=3)
241
+ return host
242
+
243
+
244
+ def _pick_wave(kernel: str, kw: dict, is_causal: bool, q: torch.Tensor, direction: str) -> bool:
245
+ if kernel == "wave":
246
+ return True
247
+ if kernel in ("cuda", "triton"):
248
+ return False
249
+ if kernel != "auto":
250
+ raise ValueError(f"kernel must be 'auto', 'wave', 'cuda' or 'triton' (got {kernel!r})")
251
+ try:
252
+ from .native_wave import native_available
253
+ if not native_available():
254
+ return False
255
+ except Exception:
256
+ return False
257
+ # the wave engine keeps its accumulator (and, in the backward, the fp32 gradients) on the device
258
+ if kw.get("low_memory") or kw.get("accumulate_on_gpu") is False:
259
+ return False
260
+ from .native_wave import native_supports
261
+ return native_supports(q.dtype, int(q.shape[-1])) # depends on the kernel set the extension was built with
262
+
263
+
264
+ # ---------------------------------------------------------------------------
265
+ # dry run
266
+ # ---------------------------------------------------------------------------
267
+ def estimate(N: int, B: int = 1, H: int = 8, D: int = 64, dtype=torch.float16, causal: bool = True,
268
+ hardware=None, direction: str = "both", print_table: bool = True) -> dict[str, Any]:
269
+ """
270
+ What would ``attention`` do for this shape on this machine, and what would it
271
+ cost? Prints (and returns) the monolithic estimate, the chosen decomposition
272
+ and the runner-up configurations, for the forward and the backward.
273
+ """
274
+ from .autoconfig import plan as _plan, hardware_from_dict, detect_hardware, _cname, GIB
275
+ hw = hardware_from_dict(hardware) if isinstance(hardware, dict) else (hardware or detect_hardware())
276
+ res: dict[str, Any] = {"N": N, "B": B, "H": H, "D": D, "dtype": str(dtype), "causal": causal, "hardware": hw.summary()}
277
+ dirs = ("fwd", "bwd") if direction == "both" else (direction,)
278
+ lines = [f"Stream-CQSA estimate for N={N:,} B={B} H={H} D={D} {str(dtype).replace('torch.', '')} {'causal' if causal else 'non-causal'}",
279
+ f" hardware: {hw.summary()}"]
280
+ itemsize = torch.tensor([], dtype=dtype).element_size()
281
+ for d in dirs:
282
+ p = _plan(N=N, B=B, H=H, D=D, dtype=dtype, causal=causal, hardware=hw, direction=d, allow_distributed=False)
283
+ mono = p.candidates[0]
284
+ feas = sorted([c for c in p.candidates if c["ok"] and c["mode"] != "mono"], key=lambda c: c["time"])[:3]
285
+ r = dict(plan=p.name(), est_time_s=p.est_time_s, est_peak_gib=p.est_peak_gib, est_host_gib=p.est_host_gib,
286
+ mono_fits=bool(mono["ok"]), mono_peak_gib=mono["peak"], mono_time_s=mono["time"], reason=p.reason,
287
+ runners_up=[dict(config=_cname(c), time_s=c["time"], peak_gib=c["peak"], host_gib=c["host"]) for c in feas],
288
+ feasible=any(c["ok"] for c in p.candidates))
289
+ res[d] = r
290
+ label = "forward" if d == "fwd" else "backward (fwd+bwd step)"
291
+ lines.append(f" {label}:")
292
+ lines.append(f" monolithic FlashAttention: {'fits' if mono['ok'] else 'does NOT fit'} ({mono['peak']:.1f} GiB), ~{mono['time']:.2f} s")
293
+ if p.mode == "mono":
294
+ lines.append(f" -> monolithic call")
295
+ elif r["feasible"]:
296
+ lines.append(f" -> Stream-CQSA {p.name()}: ~{p.est_time_s:.1f} s, {p.est_peak_gib:.1f} GiB on the device"
297
+ + (f", {p.est_host_gib:.0f} GiB host" if p.est_host_gib else ""))
298
+ for c in feas[1:]:
299
+ lines.append(f" also: {_cname(c)} ~{c['time']:.1f} s, {c['peak']:.1f} GiB")
300
+ else:
301
+ floor = (4 + 4) * N * B * H * D / GIB
302
+ lines.append(f" -> nothing fits: the fp32 output + accumulator alone are {floor:.1f} GiB on the device; "
303
+ f"reduce B/H/D, add host memory, or use more devices")
304
+ if print_table:
305
+ print("\n".join(lines), flush=True)
306
+ res["text"] = "\n".join(lines)
307
+ return res
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # monkeypatch F.scaled_dot_product_attention
312
+ # ---------------------------------------------------------------------------
313
+ _ORIGINAL_SDPA = None
314
+ _PATCH_MIN_TOKENS = 65536
315
+ _TRUE_SDPA = F.scaled_dot_product_attention # captured at import, before any patch
316
+ _INSIDE = [0] # re-entrancy: calls made by the engines go to the original
317
+
318
+
319
+ def _patched_sdpa(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None, enable_gqa=False):
320
+ orig = _ORIGINAL_SDPA or _TRUE_SDPA
321
+ supported = (attn_mask is None and not dropout_p and not enable_gqa and query.dim() == 4
322
+ and query.dtype in (torch.float16, torch.bfloat16) and key.shape == query.shape == value.shape
323
+ and query.shape[-2] >= _PATCH_MIN_TOKENS and _INSIDE[0] == 0)
324
+ if not supported:
325
+ return orig(query, key, value, attn_mask=attn_mask, dropout_p=dropout_p, is_causal=is_causal, scale=scale,
326
+ **({"enable_gqa": enable_gqa} if enable_gqa else {}))
327
+ return attention(query, key, value, is_causal=is_causal, scale=scale)
328
+
329
+
330
+ def patch_sdpa(min_tokens: int = 65536) -> None:
331
+ """
332
+ Route ``torch.nn.functional.scaled_dot_product_attention`` through
333
+ ``stream_cqsa.attention`` for the calls it handles (4-D fp16/bf16, no mask,
334
+ no dropout, no GQA, N >= ``min_tokens``); everything else goes to the original.
335
+ Below the memory boundary the routed call is still the monolithic kernel, so
336
+ the patch changes nothing until a call would have run out of memory.
337
+ """
338
+ global _ORIGINAL_SDPA, _PATCH_MIN_TOKENS
339
+ _PATCH_MIN_TOKENS = int(min_tokens)
340
+ if _ORIGINAL_SDPA is None:
341
+ _ORIGINAL_SDPA = F.scaled_dot_product_attention
342
+ F.scaled_dot_product_attention = _patched_sdpa
343
+ torch.nn.functional.scaled_dot_product_attention = _patched_sdpa
344
+
345
+
346
+ def unpatch_sdpa() -> None:
347
+ global _ORIGINAL_SDPA
348
+ if _ORIGINAL_SDPA is not None:
349
+ F.scaled_dot_product_attention = _ORIGINAL_SDPA
350
+ torch.nn.functional.scaled_dot_product_attention = _ORIGINAL_SDPA
351
+ _ORIGINAL_SDPA = None
352
+
353
+
354
+ @contextlib.contextmanager
355
+ def patched_sdpa(min_tokens: int = 65536):
356
+ """``with stream_cqsa.patched_sdpa(): model(...)``"""
357
+ was = _ORIGINAL_SDPA is not None
358
+ patch_sdpa(min_tokens)
359
+ try:
360
+ yield
361
+ finally:
362
+ if not was:
363
+ unpatch_sdpa()
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Sequence, Tuple
4
+
5
+ import torch
6
+
7
+
8
+ def _group_bits_to_dense_mask(
9
+ *,
10
+ group_bits: torch.Tensor,
11
+ device: torch.device,
12
+ ) -> torch.Tensor:
13
+ """
14
+ Build dense local mask [L, L] from per-token int64 group bits.
15
+ True means masked.
16
+ """
17
+ bits = group_bits.to(device=device, dtype=torch.int64).contiguous()
18
+ bits_row = bits.view(bits.numel(), 1)
19
+ bits_col = bits.view(1, bits.numel())
20
+ return torch.bitwise_and(bits_row, bits_col).ne(0)
21
+
22
+
23
+ def _group_runs_to_dense_mask(
24
+ *,
25
+ local_size: int,
26
+ group_runs: Sequence[Sequence[Tuple[int, int]]],
27
+ device: torch.device,
28
+ ) -> torch.Tensor:
29
+ """
30
+ Build dense local mask [L, L] where True means masked.
31
+ """
32
+ mask = torch.zeros((int(local_size), int(local_size)), dtype=torch.bool, device=device)
33
+ for runs in group_runs:
34
+ idx_parts: List[torch.Tensor] = []
35
+ for s, e in runs:
36
+ si, ei = int(s), int(e)
37
+ if ei > si:
38
+ idx_parts.append(torch.arange(si, ei, device=device, dtype=torch.long))
39
+ if len(idx_parts) == 0:
40
+ continue
41
+ idx = idx_parts[0] if len(idx_parts) == 1 else torch.cat(idx_parts, dim=0)
42
+ mask[idx.unsqueeze(1), idx.unsqueeze(0)] = True
43
+ return mask
44
+
45
+
46
+ def custom_attn(
47
+ *,
48
+ q: torch.Tensor,
49
+ k: torch.Tensor,
50
+ v: torch.Tensor,
51
+ cqs_mask: Dict[str, Any],
52
+ softmax_scale: float,
53
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
54
+ """
55
+ Naive reference subsequence attention plugin.
56
+
57
+ Steps:
58
+ 1) R_i = (Q @ K^T) * scale
59
+ 2) apply CQS mask M_i on R_i (masked positions -> -inf)
60
+ 3) P_i = exp(R_i) (no softmax normalization)
61
+ 4) Den_i = row_sum(P_i)
62
+ 5) Num_i = P_i @ V
63
+
64
+ Returns:
65
+ - Num_i: [B, L, H, D] float32
66
+ - Den_i: [B, H, L] float32
67
+ """
68
+ local_size = int(cqs_mask["local_size"])
69
+ group_bits_cpu = cqs_mask.get("group_bits_cpu", None)
70
+ if isinstance(group_bits_cpu, torch.Tensor):
71
+ M_i = _group_bits_to_dense_mask(
72
+ group_bits=group_bits_cpu,
73
+ device=q.device,
74
+ )
75
+ else:
76
+ M_i = _group_runs_to_dense_mask(
77
+ local_size=local_size,
78
+ group_runs=cqs_mask["group_runs"],
79
+ device=q.device,
80
+ ) # [L, L], True means masked
81
+
82
+ q_bhld = q.transpose(1, 2).float() # [B, H, L, D]
83
+ k_bhld = k.transpose(1, 2).float() # [B, H, L, D]
84
+ v_bhld = v.transpose(1, 2).float() # [B, H, L, D]
85
+
86
+ R_i = torch.matmul(q_bhld, k_bhld.transpose(-2, -1)) * float(softmax_scale) # [B, H, L, L]
87
+ if bool(M_i.any().item()):
88
+ R_i = R_i.masked_fill(M_i.unsqueeze(0).unsqueeze(0), float("-inf"))
89
+
90
+ P_i = torch.exp(R_i) # [B, H, L, L]
91
+ P_i = torch.nan_to_num(P_i, nan=0.0, posinf=0.0, neginf=0.0)
92
+
93
+ Den_i = P_i.sum(dim=-1) # [B, H, L]
94
+ Num_i = torch.matmul(P_i, v_bhld).transpose(1, 2).contiguous() # [B, L, H, D]
95
+ Num_i = torch.nan_to_num(Num_i, nan=0.0, posinf=0.0, neginf=0.0)
96
+ Den_i = torch.nan_to_num(Den_i, nan=0.0, posinf=0.0, neginf=0.0)
97
+ return Num_i, Den_i