Instructions to use kernels-community/metal-flash-sdpa with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Kernels
How to use kernels-community/metal-flash-sdpa with Kernels:
# !pip install kernels from kernels import get_kernel kernel = get_kernel("kernels-community/metal-flash-sdpa") - Notebooks
- Google Colab
- Kaggle
[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 |
-
|
| 4 |
|
| 5 |
[torch]
|
| 6 |
src = [
|
|
@@ -10,9 +10,9 @@ src = [
|
|
| 10 |
|
| 11 |
[kernel.sdpa_metal]
|
| 12 |
backend = "metal"
|
|
|
|
| 13 |
src = [
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 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 =
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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);
|