Skip to content

Commit 7ecfe67

Browse files
GregoryComerfacebook-github-bot
authored andcommitted
Support bf16/fp16 activations in CPU SDPA (#20611)
Summary: Add support for reduced-precision (bf16 + f16) activations in the LLM SDPA op. Bf16 support is the main goal, but I wired up f16 at the same time, as it's straightforward. This initial implementation just exposes the actual operator-level support and does not yet wire e2e through ET-LLM or anything else. It also initially only supports our simple CPU BLAS implementation. I'll wire up MKL as a follow up. I'm probably going to leverage Kleidi for the ARM GEMM kernels. Apple's Accelerate doesn't support BF16 gemms as far as I can tell. Kleidi will be fast on SME2 (M4/A18+) but won't hit AMX on M1-M3. I think that's fine, though. The i8mm/dot kernels should be sufficient. We can use BNNS if we need to close the gap. ## Perplexity Measured on wikitext, M4 Max, ~40k token sanity check, 1000 token windows | f32 | bf16 | delta | | -- | -- | -- | | 26.453 | 26.671 | +0.22 (+0.82%) | ## Performance It is slow because it uses the fallback GEMM (as described above). Will wire Kleidi + MKL as a follow-up. Nothing uses bf16 SDPA yet, so this is fine. (SDPA op timing - qwen3-1.7b) | Device | Context | Phase | f32 (ms) | bf16 (ms) | bf16/f32 | |--------|--------:|---------|---------:|----------:|---------:| | M4 Max | 128 | Prefill | 1.855 | 10.180 | 5.5× | | M4 Max | 128 | Decode | 0.112 | 0.074 | 0.66× | | M4 Max | 1024 | Prefill | 76.930 | 1254.800 | 16.3× | | M4 Max | 1024 | Decode | 0.800 | 1.201 | 1.5× | | S25 | 128 | Prefill | 3.924 | 43.760 | 11.2× | | S25 | 128 | Decode | 0.245 | 0.329 | 1.3× | | S25 | 1024 | Prefill | 168.340 | 2790.700 | 16.6× | | S25 | 1024 | Decode | 1.614 | 2.698 | 1.7× | Differential Revision: D110246161 Pulled By: GregoryComer
1 parent 8965e51 commit 7ecfe67

9 files changed

Lines changed: 362 additions & 60 deletions

File tree

extension/llm/custom_ops/custom_ops.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,16 @@ def _validate_params(
7575
value.dim() == 4
7676
), f"Expected value to be 4 dimensional but got {value.dim()} dimensions."
7777

78+
supported_dtypes = (torch.float32, torch.bfloat16, torch.float16)
7879
assert (
79-
query.dtype == torch.float32
80-
), f"Expected query to be float32 but got {query.dtype}"
81-
assert key.dtype == torch.float32, f"Expected key to be float32 but got {key.dtype}"
80+
query.dtype in supported_dtypes
81+
), f"Expected query to be float32, bfloat16, or float16 but got {query.dtype}"
8282
assert (
83-
value.dtype == torch.float32
84-
), f"Expected value to be float32 but got {value.dtype}"
83+
key.dtype == query.dtype
84+
), f"Expected key to have the same dtype as query but got {key.dtype}"
85+
assert (
86+
value.dtype == query.dtype
87+
), f"Expected value to have the same dtype as query but got {value.dtype}"
8588

8689
assert (
8790
key_cache.dim() == 4
@@ -91,11 +94,11 @@ def _validate_params(
9194
), f"Expected value_cache to be 4 dimensional but got {value_cache.dim()}"
9295

9396
assert (
94-
key_cache.dtype == torch.float32
95-
), f"Expected key_cache to be float32 but got {key_cache.dtype}"
97+
key_cache.dtype == query.dtype
98+
), f"Expected key_cache to have the same dtype as query but got {key_cache.dtype}"
9699
assert (
97-
value_cache.dtype == torch.float32
98-
), f"Expected value_cache to be float32 but got {value_cache.dtype}"
100+
value_cache.dtype == query.dtype
101+
), f"Expected value_cache to have the same dtype as query but got {value_cache.dtype}"
99102

100103
assert (
101104
key_cache.size() == value_cache.size()

extension/llm/custom_ops/op_sdpa.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ bool validate_flash_attention_args(
4545

4646
ET_CHECK_OR_RETURN_FALSE(
4747
(query.scalar_type() == ScalarType::Float) ||
48+
(query.scalar_type() == ScalarType::Half) ||
49+
(query.scalar_type() == ScalarType::BFloat16) ||
4850
(query.scalar_type() == ScalarType::Char),
49-
"Query must be Float type");
51+
"Query must be Float, Half, BFloat16, or Char type");
5052

5153
ET_CHECK_OR_RETURN_FALSE(
5254
(query.scalar_type() == key.scalar_type()) &&
@@ -266,7 +268,7 @@ Tensor& flash_attention_kernel_out(
266268

267269
auto seq_len = query.size(2);
268270

269-
ET_SWITCH_FLOAT_TYPES(
271+
ET_SWITCH_FLOATHBF16_TYPES(
270272
query.scalar_type(), ctx, "flash_attention", CTYPE, [&] {
271273
// TODO we need to re-evaluate this for ARM CPUs
272274
// And there can be many so instead of templatizing
@@ -414,7 +416,7 @@ Tensor& custom_sdpa_out_impl(
414416

415417
// TODO(task): replace the template param selection logic
416418
// with whatever apprpriately makes more sense for
417-
ET_SWITCH_FLOAT_TYPES(
419+
ET_SWITCH_FLOATHBF16_TYPES(
418420
output.scalar_type(), ctx, "flash_attention", CTYPE, [&] {
419421
// TODO we need to re-evaluate this for ARM CPUs
420422
// And there can be many so instead of templatizing

extension/llm/custom_ops/op_sdpa_impl.h

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ void _q_at_k_gemm(
7474
accum_t* qk_data) {
7575
ET_CHECK_MSG(q_data.dtype == k_data.dtype, "q and k must have same dtype");
7676
ET_CHECK_MSG(
77-
q_data.dtype == ScalarType::Char || q_data.dtype == ScalarType::Float,
78-
"q and k must be either int8 or float");
77+
q_data.dtype == ScalarType::Char || q_data.dtype == ScalarType::Float ||
78+
q_data.dtype == ScalarType::Half ||
79+
q_data.dtype == ScalarType::BFloat16,
80+
"q and k must be int8, float, half, or bfloat16");
7981
if (q_data.dtype == ScalarType::Char) {
8082
if constexpr (std::is_same<accum_t, float>::value) {
8183
int a_stride_m_tmp, b_stride_n_tmp;
@@ -103,6 +105,35 @@ void _q_at_k_gemm(
103105
ET_CHECK_MSG(
104106
false, "Accumulation in dtype other than float not supported yet");
105107
}
108+
} else if (
109+
q_data.dtype == ScalarType::BFloat16 ||
110+
q_data.dtype == ScalarType::Half) {
111+
if constexpr (std::is_same<accum_t, float>::value) {
112+
auto do_gemm = [&](auto rt_tag) {
113+
using rt = decltype(rt_tag);
114+
::executorch::cpublas::gemm(
115+
::executorch::cpublas::TransposeType::Transpose,
116+
::executorch::cpublas::TransposeType::NoTranspose,
117+
k_n,
118+
q_m,
119+
qk_k,
120+
1.0f,
121+
static_cast<const rt*>(k_data.data),
122+
k_stride_n,
123+
static_cast<const rt*>(q_data.data),
124+
q_stride_m,
125+
0.0f,
126+
qk_data,
127+
k_n);
128+
};
129+
if (q_data.dtype == ScalarType::BFloat16) {
130+
do_gemm(::executorch::aten::BFloat16{});
131+
} else {
132+
do_gemm(::executorch::aten::Half{});
133+
}
134+
} else {
135+
ET_CHECK_MSG(false, "Reduced-precision q@k requires float accumulation");
136+
}
106137
} else {
107138
::executorch::cpublas::gemm(
108139
::executorch::cpublas::TransposeType::Transpose,
@@ -251,7 +282,7 @@ void _qk_at_v_gemm(
251282
const int64_t m,
252283
const int64_t n,
253284
const int64_t k,
254-
const accum_t* qk_data,
285+
const void* qk_data,
255286
const int64_t qk_stride_m,
256287
const MaybeQuantizedMatrixData& v_data,
257288
const int64_t v_stride_n,
@@ -261,14 +292,15 @@ void _qk_at_v_gemm(
261292
accum_t* buf_qdq_ptr) {
262293
if (v_data.dtype == ScalarType::Char) {
263294
if constexpr (std::is_same<accum_t, float>::value) {
295+
const float* qk = static_cast<const float*>(qk_data);
264296
if (m > 4) {
265297
// For larger batch sizes, dequantize and use BLAS for better
266298
// performance
267299
dequant_and_gemm(
268300
m,
269301
n,
270302
k,
271-
const_cast<float*>(qk_data),
303+
const_cast<float*>(qk),
272304
qk_stride_m,
273305
v_data,
274306
v_stride_n,
@@ -286,7 +318,7 @@ void _qk_at_v_gemm(
286318
m,
287319
n,
288320
k,
289-
qk_data,
321+
qk,
290322
qk_stride_m /*lhs_stride_m*/,
291323
static_cast<const int8_t*>(v_data.data),
292324
v_stride_n /*rhs_stride_n*/,
@@ -301,6 +333,37 @@ void _qk_at_v_gemm(
301333
ET_CHECK_MSG(
302334
false, "Accumulation in dtype other than float not supported yet");
303335
}
336+
} else if (
337+
v_data.dtype == ScalarType::BFloat16 ||
338+
v_data.dtype == ScalarType::Half) {
339+
// qk has been cast to the activation dtype (see qk_reduced_data); both
340+
// operands are reduced precision and accumulate into the float output.
341+
if constexpr (std::is_same<accum_t, float>::value) {
342+
auto do_gemm = [&](auto rt_tag) {
343+
using rt = decltype(rt_tag);
344+
::executorch::cpublas::gemm(
345+
::executorch::cpublas::TransposeType::NoTranspose,
346+
::executorch::cpublas::TransposeType::NoTranspose,
347+
n,
348+
m,
349+
k,
350+
1.0f,
351+
static_cast<const rt*>(v_data.data),
352+
v_stride_n,
353+
static_cast<const rt*>(qk_data),
354+
qk_stride_m,
355+
beta,
356+
o_data,
357+
o_stride_m);
358+
};
359+
if (v_data.dtype == ScalarType::BFloat16) {
360+
do_gemm(::executorch::aten::BFloat16{});
361+
} else {
362+
do_gemm(::executorch::aten::Half{});
363+
}
364+
} else {
365+
ET_CHECK_MSG(false, "Reduced-precision qk@v requires float accumulation");
366+
}
304367
} else {
305368
::executorch::cpublas::gemm(
306369
::executorch::cpublas::TransposeType::NoTranspose,
@@ -311,7 +374,7 @@ void _qk_at_v_gemm(
311374
static_cast<accum_t>(1),
312375
static_cast<const accum_t*>(v_data.data),
313376
v_stride_n,
314-
qk_data,
377+
static_cast<const accum_t*>(qk_data),
315378
qk_stride_m,
316379
beta,
317380
o_data,
@@ -572,11 +635,9 @@ void cpu_flash_attention(
572635
constexpr bool is_reduced_type =
573636
::executorch::runtime::is_reduced_floating_point_v<scalar_t>;
574637

575-
ET_CHECK_MSG(
576-
!is_reduced_type, "FlashAttention does not support reduced types.");
577-
// Figure out mixed precision a little later
578-
// using accum_t = at::opmath_type<scalar_t>;
579-
using accum_t = scalar_t;
638+
// Reduced-precision (bf16/fp16) activations accumulate in float: the two
639+
// matmuls run as reduced-in/float-out gemms and the softmax stays in float.
640+
using accum_t = std::conditional_t<is_reduced_type, float, scalar_t>;
580641
using Vec = vec::Vectorized<accum_t>;
581642
accum_t scaling_factor = static_cast<accum_t>(calculate_scale(query, scale));
582643

@@ -774,7 +835,19 @@ void cpu_flash_attention(
774835
} else {
775836
buf = scratch.get();
776837
}
838+
std::unique_ptr<char[]> allocated_buf_reduced;
777839
void* buf_reduced = nullptr;
840+
if (is_reduced_type) {
841+
int64_t size_reduced_bytes =
842+
qSplitSize * kvSplitSize * num_thread * sizeof(scalar_t);
843+
Result<void*> scratch_reduced = ctx.allocate_temp(size_reduced_bytes, 64);
844+
if (!scratch_reduced.ok()) {
845+
allocated_buf_reduced = std::make_unique<char[]>(size_reduced_bytes);
846+
buf_reduced = allocated_buf_reduced.get();
847+
} else {
848+
buf_reduced = scratch_reduced.get();
849+
}
850+
}
778851
int64_t size_per_thread_qdq_vec = kvSplitSize * headSize;
779852
// Lets align size_per_thread_qdq_vec to 64 bytes, for coalesced cache reads,
780853
// by padding with right number of per thread elements
@@ -1012,18 +1085,16 @@ void cpu_flash_attention(
10121085
if (tmp_max == -std::numeric_limits<accum_t>::infinity()) {
10131086
// to avoid `nan = exp2f(-inf - (-inf))`
10141087
fill_stub(
1015-
conditional_data_ptr(qk_data, qk_reduced_data) +
1016-
row * kvBlockSize,
1017-
static_cast<scalar_t>(0),
1088+
qk_data + row * kvBlockSize,
1089+
static_cast<accum_t>(0),
10181090
kvBlockSize);
10191091
} else {
10201092
// qk <- exp(qk - max) and sum per row
10211093
tmp_sum = tmp_max;
10221094
_exp_reduce_sum_fusion_kernel(
10231095
qk_data + row * kvBlockSize,
10241096
kvBlockSize,
1025-
conditional_data_ptr(qk_data, qk_reduced_data) +
1026-
row * kvBlockSize,
1097+
qk_data + row * kvBlockSize,
10271098
tmp_sum);
10281099
// exp_tmp <- exp(max[row] - max)
10291100
exp_tmp = std::exp(qk_max_data[row] - tmp_max);
@@ -1065,12 +1136,23 @@ void cpu_flash_attention(
10651136
headSize,
10661137
v_quant_params_StrideN,
10671138
value.scalar_type());
1139+
// For reduced-precision activations the attention weights are cast to
1140+
// the activation dtype so that Softmax(q @ k.T) @ v runs as a
1141+
// reduced-in/float-out gemm matching the value matrix.
1142+
const void* qk_gemm_data = qk_data;
1143+
if constexpr (is_reduced_type) {
1144+
if (!is_quantized_sdpa) {
1145+
vec::convert<accum_t, scalar_t>(
1146+
qk_data, qk_reduced_data, qBlockSize * kvBlockSize);
1147+
qk_gemm_data = qk_reduced_data;
1148+
}
1149+
}
10681150
// Calculate Softmax(q @ k.T) @ v
10691151
_qk_at_v_gemm<accum_t>(
10701152
qBlockSize,
10711153
headSize,
10721154
kvBlockSize,
1073-
qk_data,
1155+
qk_gemm_data,
10741156
kvBlockSize,
10751157
v_sub_matrix_data,
10761158
vStrideN,
@@ -1083,12 +1165,20 @@ void cpu_flash_attention(
10831165
// reorder MHA output with strides
10841166
for (int64_t row = 0; row < qBlockSize; ++row) {
10851167
accum_t sum_reciprocal = 1 / qk_sum_data[row];
1086-
vec::map<scalar_t>(
1087-
[sum_reciprocal](Vec x) { return x * Vec(sum_reciprocal); },
1088-
out_data + i * oStrideB + j * oStrideH + m * oStrideM +
1089-
row * oStrideM,
1090-
dst_data + row * headSize,
1091-
headSize);
1168+
scalar_t* out_row = out_data + i * oStrideB + j * oStrideH +
1169+
m * oStrideM + row * oStrideM;
1170+
const accum_t* dst_row = dst_data + row * headSize;
1171+
if constexpr (is_reduced_type) {
1172+
for (int64_t d = 0; d < headSize; ++d) {
1173+
out_row[d] = static_cast<scalar_t>(dst_row[d] * sum_reciprocal);
1174+
}
1175+
} else {
1176+
vec::map<scalar_t>(
1177+
[sum_reciprocal](Vec x) { return x * Vec(sum_reciprocal); },
1178+
out_row,
1179+
dst_row,
1180+
headSize);
1181+
}
10921182
}
10931183
// Move to the next query
10941184
data_index_step(i, batchSize, j, num_head, k, qSlice);

0 commit comments

Comments
 (0)