Skip to content

Commit 4cf0cf9

Browse files
committed
Support bf16/fp16 activations in CPU SDPA
Activate the dormant mixed-precision path in cpu_flash_attention, using float accumulation for reduced-precision (bf16/fp16) activations, and add a reduced-in/float-out gemm to cpublas to back the two matmuls. Validation and dispatch now accept Half/BFloat16 alongside Float, and the Python meta accepts bf16/fp16. Review order: start with kernels/optimized/blas (the new bf16/fp16 -> fp32 gemm primitive and the out_t threading), then op_sdpa_impl.h (the kernel that uses it), then the validation/dispatch in op_sdpa.cpp and the Python meta in custom_ops.py, then the C++ and Python tests. Authored with Claude Code.
1 parent 39c0df6 commit 4cf0cf9

8 files changed

Lines changed: 384 additions & 59 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: 136 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,46 @@ 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+
if (q_data.dtype == ScalarType::BFloat16) {
113+
using rt = ::executorch::aten::BFloat16;
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+
} else {
129+
using rt = ::executorch::aten::Half;
130+
::executorch::cpublas::gemm(
131+
::executorch::cpublas::TransposeType::Transpose,
132+
::executorch::cpublas::TransposeType::NoTranspose,
133+
k_n,
134+
q_m,
135+
qk_k,
136+
1.0f,
137+
static_cast<const rt*>(k_data.data),
138+
k_stride_n,
139+
static_cast<const rt*>(q_data.data),
140+
q_stride_m,
141+
0.0f,
142+
qk_data,
143+
k_n);
144+
}
145+
} else {
146+
ET_CHECK_MSG(false, "Reduced-precision q@k requires float accumulation");
147+
}
106148
} else {
107149
::executorch::cpublas::gemm(
108150
::executorch::cpublas::TransposeType::Transpose,
@@ -251,7 +293,7 @@ void _qk_at_v_gemm(
251293
const int64_t m,
252294
const int64_t n,
253295
const int64_t k,
254-
const accum_t* qk_data,
296+
const void* qk_data,
255297
const int64_t qk_stride_m,
256298
const MaybeQuantizedMatrixData& v_data,
257299
const int64_t v_stride_n,
@@ -261,14 +303,15 @@ void _qk_at_v_gemm(
261303
accum_t* buf_qdq_ptr) {
262304
if (v_data.dtype == ScalarType::Char) {
263305
if constexpr (std::is_same<accum_t, float>::value) {
306+
const float* qk = static_cast<const float*>(qk_data);
264307
if (m > 4) {
265308
// For larger batch sizes, dequantize and use BLAS for better
266309
// performance
267310
dequant_and_gemm(
268311
m,
269312
n,
270313
k,
271-
const_cast<float*>(qk_data),
314+
const_cast<float*>(qk),
272315
qk_stride_m,
273316
v_data,
274317
v_stride_n,
@@ -286,7 +329,7 @@ void _qk_at_v_gemm(
286329
m,
287330
n,
288331
k,
289-
qk_data,
332+
qk,
290333
qk_stride_m /*lhs_stride_m*/,
291334
static_cast<const int8_t*>(v_data.data),
292335
v_stride_n /*rhs_stride_n*/,
@@ -301,6 +344,48 @@ void _qk_at_v_gemm(
301344
ET_CHECK_MSG(
302345
false, "Accumulation in dtype other than float not supported yet");
303346
}
347+
} else if (
348+
v_data.dtype == ScalarType::BFloat16 ||
349+
v_data.dtype == ScalarType::Half) {
350+
// qk has been cast to the activation dtype (see qk_reduced_data); both
351+
// operands are reduced precision and accumulate into the float output.
352+
if constexpr (std::is_same<accum_t, float>::value) {
353+
if (v_data.dtype == ScalarType::BFloat16) {
354+
using rt = ::executorch::aten::BFloat16;
355+
::executorch::cpublas::gemm(
356+
::executorch::cpublas::TransposeType::NoTranspose,
357+
::executorch::cpublas::TransposeType::NoTranspose,
358+
n,
359+
m,
360+
k,
361+
1.0f,
362+
static_cast<const rt*>(v_data.data),
363+
v_stride_n,
364+
static_cast<const rt*>(qk_data),
365+
qk_stride_m,
366+
beta,
367+
o_data,
368+
o_stride_m);
369+
} else {
370+
using rt = ::executorch::aten::Half;
371+
::executorch::cpublas::gemm(
372+
::executorch::cpublas::TransposeType::NoTranspose,
373+
::executorch::cpublas::TransposeType::NoTranspose,
374+
n,
375+
m,
376+
k,
377+
1.0f,
378+
static_cast<const rt*>(v_data.data),
379+
v_stride_n,
380+
static_cast<const rt*>(qk_data),
381+
qk_stride_m,
382+
beta,
383+
o_data,
384+
o_stride_m);
385+
}
386+
} else {
387+
ET_CHECK_MSG(false, "Reduced-precision qk@v requires float accumulation");
388+
}
304389
} else {
305390
::executorch::cpublas::gemm(
306391
::executorch::cpublas::TransposeType::NoTranspose,
@@ -311,7 +396,7 @@ void _qk_at_v_gemm(
311396
static_cast<accum_t>(1),
312397
static_cast<const accum_t*>(v_data.data),
313398
v_stride_n,
314-
qk_data,
399+
static_cast<const accum_t*>(qk_data),
315400
qk_stride_m,
316401
beta,
317402
o_data,
@@ -572,11 +657,9 @@ void cpu_flash_attention(
572657
constexpr bool is_reduced_type =
573658
::executorch::runtime::is_reduced_floating_point_v<scalar_t>;
574659

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;
660+
// Reduced-precision (bf16/fp16) activations accumulate in float: the two
661+
// matmuls run as reduced-in/float-out gemms and the softmax stays in float.
662+
using accum_t = std::conditional_t<is_reduced_type, float, scalar_t>;
580663
using Vec = vec::Vectorized<accum_t>;
581664
accum_t scaling_factor = static_cast<accum_t>(calculate_scale(query, scale));
582665

@@ -774,7 +857,19 @@ void cpu_flash_attention(
774857
} else {
775858
buf = scratch.get();
776859
}
860+
std::unique_ptr<char[]> allocated_buf_reduced;
777861
void* buf_reduced = nullptr;
862+
if (is_reduced_type) {
863+
int64_t size_reduced_bytes =
864+
qSplitSize * kvSplitSize * num_thread * sizeof(scalar_t);
865+
Result<void*> scratch_reduced = ctx.allocate_temp(size_reduced_bytes, 64);
866+
if (!scratch_reduced.ok()) {
867+
allocated_buf_reduced = std::make_unique<char[]>(size_reduced_bytes);
868+
buf_reduced = allocated_buf_reduced.get();
869+
} else {
870+
buf_reduced = scratch_reduced.get();
871+
}
872+
}
778873
int64_t size_per_thread_qdq_vec = kvSplitSize * headSize;
779874
// Lets align size_per_thread_qdq_vec to 64 bytes, for coalesced cache reads,
780875
// by padding with right number of per thread elements
@@ -1015,18 +1110,16 @@ void cpu_flash_attention(
10151110
if (tmp_max == -std::numeric_limits<accum_t>::infinity()) {
10161111
// to avoid `nan = exp2f(-inf - (-inf))`
10171112
fill_stub(
1018-
conditional_data_ptr(qk_data, qk_reduced_data) +
1019-
row * kvBlockSize,
1020-
static_cast<scalar_t>(0),
1113+
qk_data + row * kvBlockSize,
1114+
static_cast<accum_t>(0),
10211115
kvBlockSize);
10221116
} else {
10231117
// qk <- exp(qk - max) and sum per row
10241118
tmp_sum = tmp_max;
10251119
_exp_reduce_sum_fusion_kernel(
10261120
qk_data + row * kvBlockSize,
10271121
kvBlockSize,
1028-
conditional_data_ptr(qk_data, qk_reduced_data) +
1029-
row * kvBlockSize,
1122+
qk_data + row * kvBlockSize,
10301123
tmp_sum);
10311124
// exp_tmp <- exp(max[row] - max)
10321125
exp_tmp = std::exp(qk_max_data[row] - tmp_max);
@@ -1068,12 +1161,24 @@ void cpu_flash_attention(
10681161
headSize,
10691162
v_quant_params_StrideN,
10701163
value.scalar_type());
1164+
// For reduced-precision activations the attention weights are cast to
1165+
// the activation dtype so that Softmax(q @ k.T) @ v runs as a
1166+
// reduced-in/float-out gemm matching the value matrix.
1167+
const void* qk_gemm_data = qk_data;
1168+
if constexpr (is_reduced_type) {
1169+
if (!is_quantized_sdpa) {
1170+
for (int64_t idx = 0; idx < qBlockSize * kvBlockSize; ++idx) {
1171+
qk_reduced_data[idx] = static_cast<scalar_t>(qk_data[idx]);
1172+
}
1173+
qk_gemm_data = qk_reduced_data;
1174+
}
1175+
}
10711176
// Calculate Softmax(q @ k.T) @ v
10721177
_qk_at_v_gemm<accum_t>(
10731178
qBlockSize,
10741179
headSize,
10751180
kvBlockSize,
1076-
qk_data,
1181+
qk_gemm_data,
10771182
kvBlockSize,
10781183
v_sub_matrix_data,
10791184
vStrideN,
@@ -1086,12 +1191,20 @@ void cpu_flash_attention(
10861191
// reorder MHA output with strides
10871192
for (int64_t row = 0; row < qBlockSize; ++row) {
10881193
accum_t sum_reciprocal = 1 / qk_sum_data[row];
1089-
vec::map<scalar_t>(
1090-
[sum_reciprocal](Vec x) { return x * Vec(sum_reciprocal); },
1091-
out_data + i * oStrideB + j * oStrideH + m * oStrideM +
1092-
row * oStrideM,
1093-
dst_data + row * headSize,
1094-
headSize);
1194+
scalar_t* out_row = out_data + i * oStrideB + j * oStrideH +
1195+
m * oStrideM + row * oStrideM;
1196+
const accum_t* dst_row = dst_data + row * headSize;
1197+
if constexpr (is_reduced_type) {
1198+
for (int64_t d = 0; d < headSize; ++d) {
1199+
out_row[d] = static_cast<scalar_t>(dst_row[d] * sum_reciprocal);
1200+
}
1201+
} else {
1202+
vec::map<scalar_t>(
1203+
[sum_reciprocal](Vec x) { return x * Vec(sum_reciprocal); },
1204+
out_row,
1205+
dst_row,
1206+
headSize);
1207+
}
10951208
}
10961209
// Move to the next query
10971210
data_index_step(i, batchSize, j, num_head, k, qSlice);

0 commit comments

Comments
 (0)