Kernels

[WIP] Add sliding-window attention support to the varlen kernel

#5
by ArthurZ HF Staff - opened
build.toml CHANGED
@@ -1,6 +1,6 @@
1
  [general]
2
  name = "metal_flash_sdpa"
3
- universal = false
4
 
5
  [torch]
6
  src = [
@@ -10,9 +10,9 @@ src = [
10
 
11
  [kernel.sdpa_metal]
12
  backend = "metal"
 
13
  src = [
14
- "sdpa-metal/scaled_dot_product_attention.mm",
15
- "sdpa-metal/scaled_dot_product_attention.metal",
16
- "sdpa-metal/common.h",
17
  ]
18
- depends = [ "torch" ]
 
1
  [general]
2
  name = "metal_flash_sdpa"
3
+ backends = ["metal"]
4
 
5
  [torch]
6
  src = [
 
10
 
11
  [kernel.sdpa_metal]
12
  backend = "metal"
13
+ depends = ["torch"]
14
  src = [
15
+ "sdpa-metal/scaled_dot_product_attention.mm",
16
+ "sdpa-metal/scaled_dot_product_attention.metal",
17
+ "sdpa-metal/common.h",
18
  ]
 
sdpa-metal/scaled_dot_product_attention.metal CHANGED
@@ -1506,6 +1506,10 @@ struct AttnParams {
1506
  int total_k_tokens; ///< Total number of key/value tokens
1507
  int max_seqlen_q; ///< Maximum query sequence length
1508
  int max_seqlen_k; ///< Maximum key/value sequence length
 
 
 
 
1509
  };
1510
 
1511
  struct AttnMaskParams {
@@ -1521,6 +1525,8 @@ constant bool align_K [[function_constant(201)]];
1521
 
1522
  constant bool has_mask [[function_constant(300)]];
1523
  constant bool do_causal [[function_constant(301)]];
 
 
1524
 
1525
  template <typename T>
1526
  struct TransformScale {
@@ -1594,6 +1600,7 @@ template <
1594
  const device MaskType* mask [[buffer(6), function_constant(has_mask)]],
1595
  const device int* cu_seqlens_q [[buffer(7)]], // Cumulative query sequence lengths
1596
  const device int* cu_seqlens_k [[buffer(8)]], // Cumulative key sequence lengths
 
1597
  uint simd_lane_id [[thread_index_in_simdgroup]],
1598
  uint simd_group_id [[simdgroup_index_in_threadgroup]],
1599
  uint3 tid [[threadgroup_position_in_grid]],
@@ -1810,8 +1817,33 @@ template <
1810
  kb_lim = min(kb_lim, (q_block_end_in_seq + BK - 1) / BK);
1811
  }
1812
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1813
  // Loop over KV seq length
1814
- for (int kb = 0; kb < kb_lim; kb++) {
1815
  // Load K block and apply scale
1816
  threadgroup_barrier(mem_flags::mem_threadgroup);
1817
 
@@ -1894,6 +1926,38 @@ template <
1894
  }
1895
  }
1896
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1897
  // Other masking as needed
1898
  if (has_mask) {
1899
  using stile_t = decltype(Stile);
@@ -1979,6 +2043,18 @@ template <
1979
  // Row max
1980
  Stile.template row_reduce<MaxOp>(new_max);
1981
 
 
 
 
 
 
 
 
 
 
 
 
 
1982
  // exp(Si - rowmax(Si))
1983
  Stile.template row_bin_op<ExpSubOp>(new_max);
1984
 
@@ -2044,6 +2120,24 @@ template <
2044
  loader_v.next();
2045
  }
2046
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2047
  // Normalize output
2048
  Otile.template row_bin_op<DivOp>(sum_score);
2049
  threadgroup_barrier(mem_flags::mem_none);
 
1506
  int total_k_tokens; ///< Total number of key/value tokens
1507
  int max_seqlen_q; ///< Maximum query sequence length
1508
  int max_seqlen_k; ///< Maximum key/value sequence length
1509
+
1510
+ // Sliding-window attention support (-1 on a side = unbounded on that side)
1511
+ int window_left; ///< Max distance into the past a query may attend
1512
+ int window_right; ///< Max distance into the future a query may attend
1513
  };
1514
 
1515
  struct AttnMaskParams {
 
1525
 
1526
  constant bool has_mask [[function_constant(300)]];
1527
  constant bool do_causal [[function_constant(301)]];
1528
+ constant bool has_window [[function_constant(302)]];
1529
+ constant bool has_sink [[function_constant(303)]];
1530
 
1531
  template <typename T>
1532
  struct TransformScale {
 
1600
  const device MaskType* mask [[buffer(6), function_constant(has_mask)]],
1601
  const device int* cu_seqlens_q [[buffer(7)]], // Cumulative query sequence lengths
1602
  const device int* cu_seqlens_k [[buffer(8)]], // Cumulative key sequence lengths
1603
+ const device float* sinks [[buffer(9), function_constant(has_sink)]], // Per-head attention-sink logits
1604
  uint simd_lane_id [[thread_index_in_simdgroup]],
1605
  uint simd_group_id [[simdgroup_index_in_threadgroup]],
1606
  uint3 tid [[threadgroup_position_in_grid]],
 
1817
  kb_lim = min(kb_lim, (q_block_end_in_seq + BK - 1) / BK);
1818
  }
1819
 
1820
+ // Restrict the key-block range to those intersecting the sliding-window band
1821
+ // for this query block. This is both the perf win (banded, not O(n^2)) and
1822
+ // avoids visiting blocks that are entirely out of band.
1823
+ int kb_start = 0;
1824
+ if (has_window) {
1825
+ int win_offset = (q_seq_len < k_seq_len) ? (k_seq_len - q_seq_len) : 0;
1826
+ int qs = block_idx * BQ + win_offset; // first query row (key coords)
1827
+ int qe = qs + q_block_size; // exclusive
1828
+ if (params->window_left >= 0) {
1829
+ int lo = qs - params->window_left;
1830
+ kb_start = lo > 0 ? lo / BK : 0;
1831
+ }
1832
+ if (params->window_right >= 0) {
1833
+ kb_lim = min(kb_lim, ((qe - 1 + params->window_right) / BK) + 1);
1834
+ }
1835
+ }
1836
+
1837
+ // Fast-forward the K/V block loaders to the first in-band block. The loaders
1838
+ // start at block 0 and only advance via next(), so skipping iterations without
1839
+ // advancing them would read the wrong blocks.
1840
+ for (int s = 0; s < kb_start; ++s) {
1841
+ loader_k.next();
1842
+ loader_v.next();
1843
+ }
1844
+
1845
  // Loop over KV seq length
1846
+ for (int kb = kb_start; kb < kb_lim; kb++) {
1847
  // Load K block and apply scale
1848
  threadgroup_barrier(mem_flags::mem_threadgroup);
1849
 
 
1926
  }
1927
  }
1928
 
1929
+ // Mask out keys outside the sliding window band [row - window_left, row + window_right]
1930
+ if (has_window) {
1931
+ using stile_t = decltype(Stile);
1932
+ using selem_t = typename stile_t::elem_type;
1933
+ constexpr auto neg_inf = -metal::numeric_limits<selem_t>::infinity();
1934
+
1935
+ const int wl = params->window_left; // -1 => unbounded into the past
1936
+ const int wr = params->window_right; // -1 => unbounded into the future
1937
+
1938
+ STEEL_PRAGMA_UNROLL
1939
+ for (short i = 0; i < stile_t::kTileRows; i++) {
1940
+ // Same row-position machinery as the causal block above.
1941
+ int row_pos = block_idx * BQ + tm + sm + (i * stile_t::kFragRows);
1942
+ if (q_seq_len < k_seq_len) {
1943
+ row_pos += (k_seq_len - q_seq_len);
1944
+ }
1945
+ STEEL_PRAGMA_UNROLL
1946
+ for (short j = 0; j < stile_t::kTileCols; j++) {
1947
+ const int col_pos_in_seq = kb * BK + sn + (j * stile_t::kFragCols);
1948
+ STEEL_PRAGMA_UNROLL
1949
+ for (short jj = 0; jj < stile_t::MMAFrag_t::kElemCols; jj++) {
1950
+ const int col = col_pos_in_seq + jj;
1951
+ const bool past_ok = (wl < 0) || ((row_pos - col) <= wl);
1952
+ const bool future_ok = (wr < 0) || ((col - row_pos) <= wr);
1953
+ if (!(past_ok && future_ok)) {
1954
+ Stile.frag_at(i, j)[jj] = neg_inf;
1955
+ }
1956
+ }
1957
+ }
1958
+ }
1959
+ }
1960
+
1961
  // Other masking as needed
1962
  if (has_mask) {
1963
  using stile_t = decltype(Stile);
 
2043
  // Row max
2044
  Stile.template row_reduce<MaxOp>(new_max);
2045
 
2046
+ // A sliding-window query row can have a key block fully masked to -inf.
2047
+ // Then new_max stays -inf and exp2(-inf - (-inf)) = NaN poisons the row.
2048
+ // Replace a -inf running max with a finite value (the prior max if any,
2049
+ // else 0): masked entries then exp2 to 0 and accumulators stay 0.
2050
+ constexpr AccumType row_ninf = -metal::numeric_limits<AccumType>::infinity();
2051
+ STEEL_PRAGMA_UNROLL
2052
+ for (short i = 0; i < kRowsPT; ++i) {
2053
+ if (new_max[i] == row_ninf) {
2054
+ new_max[i] = (max_score[i] == row_ninf) ? AccumType(0) : max_score[i];
2055
+ }
2056
+ }
2057
+
2058
  // exp(Si - rowmax(Si))
2059
  Stile.template row_bin_op<ExpSubOp>(new_max);
2060
 
 
2120
  loader_v.next();
2121
  }
2122
 
2123
+ // Attention sink: an extra per-head logit that only enters the softmax
2124
+ // denominator (its "value" is zero), matching gpt-oss s_aux. Scores live in
2125
+ // the base-2 domain (scale folds in log2(e)), so convert the raw sink the
2126
+ // same way and re-fold the running max for numerical stability.
2127
+ if (has_sink) {
2128
+ const AccumType sink_log2 = static_cast<AccumType>(sinks[head_idx]) * 1.44269504089f;
2129
+ AccumType sink_factor[kRowsPT];
2130
+ STEEL_PRAGMA_UNROLL
2131
+ for (short i = 0; i < kRowsPT; ++i) {
2132
+ const AccumType m = metal::max(max_score[i], sink_log2);
2133
+ const AccumType resc = fast::exp2(max_score[i] - m);
2134
+ sum_score[i] = sum_score[i] * resc + fast::exp2(sink_log2 - m);
2135
+ sink_factor[i] = resc;
2136
+ max_score[i] = m;
2137
+ }
2138
+ Otile.template row_bin_op<MulOp>(sink_factor);
2139
+ }
2140
+
2141
  // Normalize output
2142
  Otile.template row_bin_op<DivOp>(sum_score);
2143
  threadgroup_barrier(mem_flags::mem_none);
sdpa-metal/scaled_dot_product_attention.mm CHANGED
@@ -69,6 +69,8 @@ struct AttnParams {
69
  int32_t total_k_tokens; // Total number of key/value tokens
70
  int32_t max_seqlen_q; // Maximum query sequence length
71
  int32_t max_seqlen_k; // Maximum key/value sequence length
 
 
72
  };
73
 
74
  // Forward declarations for kernel implementations
@@ -86,7 +88,10 @@ void call_flash_attention_varlen(
86
  int64_t max_seqlen_k,
87
  bool do_causal,
88
  double scale,
89
- double softcapping);
 
 
 
90
 
91
 
92
  void flash_attention_varlen(
@@ -100,7 +105,10 @@ void flash_attention_varlen(
100
  int64_t max_seqlen_k, // Maximum key sequence length
101
  bool do_causal, // Whether to use causal mask
102
  double scale, // Attention scale
103
- double softcapping) { // Softcapping value
 
 
 
104
 
105
  try {
106
  // Get device and stream
@@ -142,9 +150,9 @@ void flash_attention_varlen(
142
  // For variable-length Flash Attention, always use the full attention kernel
143
 
144
  // Call the Flash Attention kernel
145
- call_flash_attention_varlen(device, cmdBuf, lib, out, query, key, value,
146
  cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k,
147
- do_causal, scale, softcapping);
148
  } catch (const std::exception& e) {
149
  throw;
150
  } catch (...) {
@@ -167,7 +175,10 @@ void call_flash_attention_varlen(
167
  int64_t max_seqlen_k,
168
  bool do_causal,
169
  double scale,
170
- double softcapping) {
 
 
 
171
 
172
  // Get dimensions
173
  int64_t total_q_tokens = query.size(0);
@@ -197,7 +208,9 @@ void call_flash_attention_varlen(
197
  params.total_k_tokens = key.size(0);
198
  params.max_seqlen_q = max_seqlen_q;
199
  params.max_seqlen_k = max_seqlen_k;
200
-
 
 
201
  // Initialize fields that might be checked but aren't used in Flash Attention
202
  params.qL = 0; // Not used in variable-length attention
203
  params.kL = 0; // Not used in variable-length attention
@@ -227,11 +240,15 @@ void call_flash_attention_varlen(
227
  // The kernel will handle the cu_seqlens internally
228
 
229
  bool has_mask = false; // Masks are not supported in Flash Attention
 
 
230
 
231
  // Setup function constants
232
  MTLFunctionConstantValues *constants = [MTLFunctionConstantValues new];
233
  [constants setConstantValue:&has_mask type:MTLDataTypeBool atIndex:300];
234
  [constants setConstantValue:&do_causal type:MTLDataTypeBool atIndex:301];
 
 
235
 
236
  // Construct kernel name based on data type and head dimension
237
  std::string kernel_name = "steel_attention_";
@@ -259,6 +276,10 @@ void call_flash_attention_varlen(
259
  at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
260
  dispatch_queue_t q = stream->queue();
261
  dispatch_sync(q, ^{
 
 
 
 
262
  id<MTLComputeCommandEncoder> encoder = [cmdBuf computeCommandEncoder];
263
  TORCH_CHECK(encoder, "Failed to create compute encoder");
264
 
@@ -297,10 +318,18 @@ void call_flash_attention_varlen(
297
  [encoder setBuffer:getMTLBufferStorage(cu_seqlens_q)
298
  offset:cu_seqlens_q.storage_offset() * cu_seqlens_q.element_size()
299
  atIndex:7];
300
- [encoder setBuffer:getMTLBufferStorage(cu_seqlens_k)
301
- offset:cu_seqlens_k.storage_offset() * cu_seqlens_k.element_size()
302
  atIndex:8];
303
 
 
 
 
 
 
 
 
 
304
  // Calculate grid dimensions
305
  // We need to process each sequence independently
306
  int64_t max_blocks_q = (max_seqlen_q + BQ - 1) / BQ;
 
69
  int32_t total_k_tokens; // Total number of key/value tokens
70
  int32_t max_seqlen_q; // Maximum query sequence length
71
  int32_t max_seqlen_k; // Maximum key/value sequence length
72
+ int32_t window_left; // Sliding window: max distance into the past (-1 = unbounded)
73
+ int32_t window_right; // Sliding window: max distance into the future (-1 = unbounded)
74
  };
75
 
76
  // Forward declarations for kernel implementations
 
88
  int64_t max_seqlen_k,
89
  bool do_causal,
90
  double scale,
91
+ double softcapping,
92
+ int64_t window_left,
93
+ int64_t window_right,
94
+ const std::optional<torch::Tensor> &sinks);
95
 
96
 
97
  void flash_attention_varlen(
 
105
  int64_t max_seqlen_k, // Maximum key sequence length
106
  bool do_causal, // Whether to use causal mask
107
  double scale, // Attention scale
108
+ double softcapping, // Softcapping value
109
+ int64_t window_left, // Sliding window past extent (-1 = unbounded)
110
+ int64_t window_right, // Sliding window future extent (-1 = unbounded)
111
+ const std::optional<torch::Tensor> &sinks) { // Per-head attention-sink logits (fp32)
112
 
113
  try {
114
  // Get device and stream
 
150
  // For variable-length Flash Attention, always use the full attention kernel
151
 
152
  // Call the Flash Attention kernel
153
+ call_flash_attention_varlen(device, cmdBuf, lib, out, query, key, value,
154
  cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k,
155
+ do_causal, scale, softcapping, window_left, window_right, sinks);
156
  } catch (const std::exception& e) {
157
  throw;
158
  } catch (...) {
 
175
  int64_t max_seqlen_k,
176
  bool do_causal,
177
  double scale,
178
+ double softcapping,
179
+ int64_t window_left,
180
+ int64_t window_right,
181
+ const std::optional<torch::Tensor> &sinks) {
182
 
183
  // Get dimensions
184
  int64_t total_q_tokens = query.size(0);
 
208
  params.total_k_tokens = key.size(0);
209
  params.max_seqlen_q = max_seqlen_q;
210
  params.max_seqlen_k = max_seqlen_k;
211
+ params.window_left = static_cast<int32_t>(window_left);
212
+ params.window_right = static_cast<int32_t>(window_right);
213
+
214
  // Initialize fields that might be checked but aren't used in Flash Attention
215
  params.qL = 0; // Not used in variable-length attention
216
  params.kL = 0; // Not used in variable-length attention
 
240
  // The kernel will handle the cu_seqlens internally
241
 
242
  bool has_mask = false; // Masks are not supported in Flash Attention
243
+ bool has_window = (window_left >= 0) || (window_right >= 0);
244
+ bool has_sink = sinks.has_value();
245
 
246
  // Setup function constants
247
  MTLFunctionConstantValues *constants = [MTLFunctionConstantValues new];
248
  [constants setConstantValue:&has_mask type:MTLDataTypeBool atIndex:300];
249
  [constants setConstantValue:&do_causal type:MTLDataTypeBool atIndex:301];
250
+ [constants setConstantValue:&has_window type:MTLDataTypeBool atIndex:302];
251
+ [constants setConstantValue:&has_sink type:MTLDataTypeBool atIndex:303];
252
 
253
  // Construct kernel name based on data type and head dimension
254
  std::string kernel_name = "steel_attention_";
 
276
  at::mps::MPSStream *stream = at::mps::getCurrentMPSStream();
277
  dispatch_queue_t q = stream->queue();
278
  dispatch_sync(q, ^{
279
+ // The MPS stream may already have an open compute encoder (e.g. coalesced
280
+ // kernels under torch.compile's inductor backend). Close it before opening
281
+ // ours, otherwise Metal asserts "A command encoder is already encoding".
282
+ stream->endKernelCoalescing();
283
  id<MTLComputeCommandEncoder> encoder = [cmdBuf computeCommandEncoder];
284
  TORCH_CHECK(encoder, "Failed to create compute encoder");
285
 
 
318
  [encoder setBuffer:getMTLBufferStorage(cu_seqlens_q)
319
  offset:cu_seqlens_q.storage_offset() * cu_seqlens_q.element_size()
320
  atIndex:7];
321
+ [encoder setBuffer:getMTLBufferStorage(cu_seqlens_k)
322
+ offset:cu_seqlens_k.storage_offset() * cu_seqlens_k.element_size()
323
  atIndex:8];
324
 
325
+ // Per-head attention-sink logits - index 9 (only bound when has_sink)
326
+ if (has_sink) {
327
+ const torch::Tensor &sinks_t = sinks.value();
328
+ [encoder setBuffer:getMTLBufferStorage(sinks_t)
329
+ offset:sinks_t.storage_offset() * sinks_t.element_size()
330
+ atIndex:9];
331
+ }
332
+
333
  // Calculate grid dimensions
334
  // We need to process each sequence independently
335
  int64_t max_blocks_q = (max_seqlen_q + BQ - 1) / BQ;
torch-ext/metal_flash_sdpa/__init__.py CHANGED
@@ -1,11 +1,13 @@
1
  from ._custom_ops import (
2
  flash_attention_varlen,
 
3
  flash_attn_varlen_func,
4
  )
5
  from ._ops import ops
6
 
7
  __all__ = [
8
  "flash_attention_varlen",
 
9
  "flash_attn_varlen_func",
10
  "ops",
11
  ]
 
1
  from ._custom_ops import (
2
  flash_attention_varlen,
3
+ flash_attn_func,
4
  flash_attn_varlen_func,
5
  )
6
  from ._ops import ops
7
 
8
  __all__ = [
9
  "flash_attention_varlen",
10
+ "flash_attn_func",
11
  "flash_attn_varlen_func",
12
  "ops",
13
  ]
torch-ext/metal_flash_sdpa/_custom_ops.py CHANGED
@@ -17,6 +17,9 @@ def flash_attention_varlen(
17
  do_causal: bool = False,
18
  scale: Optional[float] = None,
19
  softcapping: float = 1.0,
 
 
 
20
  ) -> None:
21
  """
22
  Flash Attention with variable-length sequences.
@@ -38,10 +41,20 @@ def flash_attention_varlen(
38
  - cu_seqlens_q and cu_seqlens_k must have dtype torch.int32 for Metal compatibility
39
  - Supported head dimensions: 32, 64, 72, 80, 96, 128
40
  - Masks are not supported
 
41
  """
42
  if scale is None:
43
  scale = query.shape[-1] ** -0.5
44
-
 
 
 
 
 
 
 
 
 
45
  ops.flash_attention_varlen(
46
  out,
47
  query,
@@ -54,6 +67,9 @@ def flash_attention_varlen(
54
  do_causal,
55
  scale,
56
  softcapping,
 
 
 
57
  )
58
 
59
  def flash_attn_varlen_func(
@@ -71,25 +87,29 @@ def flash_attn_varlen_func(
71
  alibi_slopes: Optional[torch.Tensor] = None,
72
  deterministic: bool = False,
73
  return_attn_probs: bool = False,
 
74
  ) -> torch.Tensor:
75
  """
76
  Flash Attention function with API compatible with the original Flash Attention.
77
 
78
  Note: This implementation does not support:
79
  - dropout
80
- - window attention
81
  - alibi slopes
82
  - returning attention probabilities
 
 
 
83
  """
84
  if dropout_p > 0:
85
  raise NotImplementedError("Dropout is not supported in this implementation")
86
- if window_size != (-1, -1):
87
- raise NotImplementedError("Window attention is not supported")
88
  if alibi_slopes is not None:
89
  raise NotImplementedError("ALiBi is not supported")
90
  if return_attn_probs:
91
  raise NotImplementedError("Returning attention probabilities is not supported")
92
-
 
 
 
93
  # Create output tensor
94
  out = torch.empty_like(q)
95
 
@@ -106,12 +126,63 @@ def flash_attn_varlen_func(
106
  do_causal=causal,
107
  scale=softmax_scale,
108
  softcapping=1.0,
 
 
 
109
  )
110
-
111
  return out
112
 
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  __all__ = [
115
  "flash_attention_varlen",
116
  "flash_attn_varlen_func",
 
117
  ]
 
17
  do_causal: bool = False,
18
  scale: Optional[float] = None,
19
  softcapping: float = 1.0,
20
+ window_left: int = -1,
21
+ window_right: int = -1,
22
+ sinks: Optional[torch.Tensor] = None,
23
  ) -> None:
24
  """
25
  Flash Attention with variable-length sequences.
 
41
  - cu_seqlens_q and cu_seqlens_k must have dtype torch.int32 for Metal compatibility
42
  - Supported head dimensions: 32, 64, 72, 80, 96, 128
43
  - Masks are not supported
44
+ - window_left / window_right bound a sliding-window band (-1 = unbounded)
45
  """
46
  if scale is None:
47
  scale = query.shape[-1] ** -0.5
48
+
49
+ # The kernel reads raw storage assuming contiguous [tokens, heads, dim]. Callers may pass
50
+ # transposed/reshaped views, so force contiguity on the inputs (out must already be contiguous).
51
+ query, key, value = query.contiguous(), key.contiguous(), value.contiguous()
52
+
53
+ # The kernel reads `sinks` as fp32 (the model keeps them in fp32). transformers' flash
54
+ # wrapper casts s_aux to the query dtype (fp16), so cast back here to avoid misreads.
55
+ if sinks is not None:
56
+ sinks = sinks.to(torch.float32).contiguous()
57
+
58
  ops.flash_attention_varlen(
59
  out,
60
  query,
 
67
  do_causal,
68
  scale,
69
  softcapping,
70
+ window_left,
71
+ window_right,
72
+ sinks,
73
  )
74
 
75
  def flash_attn_varlen_func(
 
87
  alibi_slopes: Optional[torch.Tensor] = None,
88
  deterministic: bool = False,
89
  return_attn_probs: bool = False,
90
+ s_aux: Optional[torch.Tensor] = None,
91
  ) -> torch.Tensor:
92
  """
93
  Flash Attention function with API compatible with the original Flash Attention.
94
 
95
  Note: This implementation does not support:
96
  - dropout
 
97
  - alibi slopes
98
  - returning attention probabilities
99
+
100
+ `window_size = (left, right)` follows the flash-attn convention: a token attends to
101
+ keys in [pos - left, pos + right]; -1 means unbounded on that side.
102
  """
103
  if dropout_p > 0:
104
  raise NotImplementedError("Dropout is not supported in this implementation")
 
 
105
  if alibi_slopes is not None:
106
  raise NotImplementedError("ALiBi is not supported")
107
  if return_attn_probs:
108
  raise NotImplementedError("Returning attention probabilities is not supported")
109
+
110
+ # Ensure contiguous so the output buffer (empty_like) and inputs match the kernel's layout.
111
+ q, k, v = q.contiguous(), k.contiguous(), v.contiguous()
112
+
113
  # Create output tensor
114
  out = torch.empty_like(q)
115
 
 
126
  do_causal=causal,
127
  scale=softmax_scale,
128
  softcapping=1.0,
129
+ window_left=window_size[0],
130
+ window_right=window_size[1],
131
+ sinks=s_aux,
132
  )
133
+
134
  return out
135
 
136
 
137
+ def flash_attn_func(
138
+ q: torch.Tensor,
139
+ k: torch.Tensor,
140
+ v: torch.Tensor,
141
+ dropout_p: float = 0.0,
142
+ softmax_scale: Optional[float] = None,
143
+ causal: bool = False,
144
+ window_size: tuple = (-1, -1),
145
+ alibi_slopes: Optional[torch.Tensor] = None,
146
+ deterministic: bool = False,
147
+ return_attn_probs: bool = False,
148
+ s_aux: Optional[torch.Tensor] = None,
149
+ **kwargs,
150
+ ) -> torch.Tensor:
151
+ """
152
+ Non-varlen entry point: q/k/v are [batch, seqlen, heads, head_dim] with uniform length.
153
+ Wraps the varlen kernel by treating each batch row as one packed sequence. Lets the kernel
154
+ be used from the standard (non-continuous-batching) attention path.
155
+ """
156
+ if dropout_p and dropout_p > 0:
157
+ raise NotImplementedError("Dropout is not supported in this implementation")
158
+ if alibi_slopes is not None:
159
+ raise NotImplementedError("ALiBi is not supported")
160
+ if return_attn_probs:
161
+ raise NotImplementedError("Returning attention probabilities is not supported")
162
+
163
+ B, S, H, D = q.shape
164
+ Sk = k.shape[1]
165
+ cu_q = torch.arange(0, (B + 1) * S, S, device=q.device, dtype=torch.int32)
166
+ cu_k = torch.arange(0, (B + 1) * Sk, Sk, device=k.device, dtype=torch.int32)
167
+ out = flash_attn_varlen_func(
168
+ q.reshape(B * S, H, D),
169
+ k.reshape(B * Sk, k.shape[2], D),
170
+ v.reshape(B * Sk, v.shape[2], D),
171
+ cu_q,
172
+ cu_k,
173
+ S,
174
+ Sk,
175
+ dropout_p=0.0,
176
+ softmax_scale=softmax_scale,
177
+ causal=causal,
178
+ window_size=window_size,
179
+ s_aux=s_aux,
180
+ )
181
+ return out.reshape(B, S, H, D)
182
+
183
+
184
  __all__ = [
185
  "flash_attention_varlen",
186
  "flash_attn_varlen_func",
187
+ "flash_attn_func",
188
  ]
torch-ext/torch_binding.cpp CHANGED
@@ -4,7 +4,7 @@
4
  #include "torch_binding.h"
5
 
6
  TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
7
- ops.def("flash_attention_varlen(Tensor! out, Tensor query, Tensor key, Tensor value, Tensor cu_seqlens_q, Tensor cu_seqlens_k, int max_seqlen_q, int max_seqlen_k, bool do_causal, float scale, float softcapping) -> ()");
8
  ops.impl("flash_attention_varlen", torch::kMPS, flash_attention_varlen);
9
  }
10
 
 
4
  #include "torch_binding.h"
5
 
6
  TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
7
+ ops.def("flash_attention_varlen(Tensor! out, Tensor query, Tensor key, Tensor value, Tensor cu_seqlens_q, Tensor cu_seqlens_k, int max_seqlen_q, int max_seqlen_k, bool do_causal, float scale, float softcapping, int window_left, int window_right, Tensor? sinks) -> ()");
8
  ops.impl("flash_attention_varlen", torch::kMPS, flash_attention_varlen);
9
  }
10
 
torch-ext/torch_binding.h CHANGED
@@ -13,4 +13,7 @@ void flash_attention_varlen(
13
  int64_t max_seqlen_k,
14
  bool do_causal,
15
  double scale,
16
- double softcapping);
 
 
 
 
13
  int64_t max_seqlen_k,
14
  bool do_causal,
15
  double scale,
16
+ double softcapping,
17
+ int64_t window_left,
18
+ int64_t window_right,
19
+ const std::optional<torch::Tensor> &sinks);