Skip to content

Commit 193c0a0

Browse files
committed
perf(cpu): productize MiniCPM5 native KV-head GQA
1 parent 08bca1c commit 193c0a0

29 files changed

Lines changed: 669 additions & 63 deletions
242 KB
Loading

mllm/backends/cpu/CMakeLists.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ if(MLLM_KERNEL_USE_THREADS AND MLLM_KERNEL_THREADS_VENDOR_OPENMP)
143143
# Apple should not use OpenMP
144144
message(ERROR "Apple platform should not use OpenMP. Pls set MLLM_KERNEL_THREADS_VENDOR_APPLE_GCD=ON")
145145
else()
146-
target_link_libraries(MllmRT PUBLIC ${OpenMP_CXX_FLAGS})
147-
target_compile_options(MllmRT PRIVATE ${OpenMP_CXX_FLAGS})
148-
target_include_directories(MllmRT PUBLIC ${OpenMP_CXX_INCLUDE_DIR})
146+
target_link_libraries(MllmCPUBackend PUBLIC ${OpenMP_CXX_FLAGS})
147+
target_compile_options(MllmCPUBackend PRIVATE ${OpenMP_CXX_FLAGS})
148+
target_include_directories(MllmCPUBackend PUBLIC ${OpenMP_CXX_INCLUDE_DIR})
149149
endif()
150150
endif()
151151

mllm/backends/cpu/CPUBackend.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "mllm/backends/cpu/ops/FlashAttn2WithSinkAndSwaOp.hpp"
2525
#include "mllm/backends/cpu/ops/GELUOp.hpp"
2626
#include "mllm/backends/cpu/ops/GatherOp.hpp"
27+
#include "mllm/backends/cpu/ops/GroupedQueryAttentionDecodeOp.hpp"
2728
#include "mllm/backends/cpu/ops/InterpolateOp.hpp"
2829
#include "mllm/backends/cpu/ops/LayerNorm2DOp.hpp"
2930
#include "mllm/backends/cpu/ops/MaskedScatterOp.hpp"
@@ -83,7 +84,7 @@ CPUBackend::CPUBackend() : Backend(kCPU, createCPUAllocator()) {
8384
CPUConv2DOpFactory, CPULayerNorm2DOpFactory, CPUInterpolateOpFactory, CPUPadOpFactory, CPUMaskedScatterOpFactory,
8485
CPUArgsortOpFactory, CPUCloneOpFactory, CPUAvgPool1dOpFactory, CPUFlashAttention2SwaSinkOpFactory,
8586
CPURadixAttnRelaxOpFactory, CPURadixAttnSwaSinkOpFactory, CPUEqualOpFactory, CPUWhereOpFactory,
86-
CPUGatherOpFactory>();
87+
CPUGatherOpFactory, CPUGroupedQueryAttentionDecodeOpFactory>();
8788
}
8889

8990
CPUBackend::~CPUBackend() {

mllm/backends/cpu/kernels/arm/linear/kai.cpp

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -394,14 +394,13 @@ void KaiLinear_f32_qai8dxp_qsi4c32p_mxk_nxk::matmul(float* __restrict__ dst, con
394394
const int m_step = ukernels_[tile_cfg].get_m_step(); // Scheduling along M
395395
const int n_step = ukernels_[tile_cfg].get_n_step(); // Scheduling along N
396396

397-
std::vector<std::pair<int, int>> tile_splits;
398-
for (int i_m_step = 0; i_m_step < M; i_m_step += m_step) {
399-
for (int i_n_step = 0; i_n_step < N; i_n_step += n_step) { tile_splits.emplace_back(i_m_step, i_n_step); }
400-
}
401-
auto tile_sizes = tile_splits.size();
397+
const int m_tiles = (M + m_step - 1) / m_step;
398+
const int n_tiles = (N + n_step - 1) / n_step;
399+
const int tile_count = m_tiles * n_tiles;
402400

403-
MLLM_CONDITIONAL_PARALLEL_FOR(thread_count > 1, thread_count, tile_idx, 0, tile_sizes, 1, {
404-
auto [i_m_step, i_n_step] = tile_splits[tile_idx];
401+
MLLM_CONDITIONAL_PARALLEL_FOR(thread_count > 1, thread_count, tile_idx, 0, tile_count, 1, {
402+
const int i_m_step = (tile_idx / n_tiles) * m_step;
403+
const int i_n_step = (tile_idx % n_tiles) * n_step;
405404

406405
// Support functions return offset in bytes
407406
const void* lhs_ptr = (const void*)((const char*)workspace + (ukernels_[tile_cfg].get_lhs_packed_offset(i_m_step, K)));
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) MLLM Team.
2+
// Licensed under the MIT License.
3+
4+
#pragma once
5+
6+
#include <algorithm>
7+
#include <cmath>
8+
#include <cstdint>
9+
#include <limits>
10+
11+
#include "mllm/backends/cpu/kernels/common/fa2_1/arch.hpp"
12+
#include "mllm/backends/cpu/kernels/common/fa2_1/impl-any.hpp"
13+
#include "mllm/core/DataTypes.hpp"
14+
#include "mllm/utils/CPUArchHelper.hpp"
15+
16+
#if defined(MLLM_HOST_ARCH_ARM64) || defined(MLLM_HOST_ARCH_ARM)
17+
#include "mllm/backends/cpu/kernels/common/fa2_1/impl-arm.hpp"
18+
#elif defined(MLLM_HOST_ARCH_X86_64) || defined(MLLM_HOST_ARCH_X86)
19+
#include "mllm/backends/cpu/kernels/common/fa2_1/impl-any-simd.hpp"
20+
#endif
21+
22+
namespace mllm::cpu::gqa_decode {
23+
24+
struct BhsdStrides {
25+
int32_t batch;
26+
int32_t head;
27+
int32_t sequence;
28+
int32_t dimension;
29+
};
30+
31+
namespace detail {
32+
33+
#if defined(MLLM_HOST_ARCH_ARM64) || defined(MLLM_HOST_ARCH_ARM)
34+
using attention_arch_tag = flash_attn2::details::arm_arch_tag;
35+
#elif defined(MLLM_HOST_ARCH_X86_64) || defined(MLLM_HOST_ARCH_X86)
36+
using attention_arch_tag = flash_attn2::details::x86_arch_tag;
37+
#else
38+
using attention_arch_tag = flash_attn2::details::any_arch_tag;
39+
#endif
40+
41+
inline bool validStrides(const BhsdStrides& strides) {
42+
return strides.batch > 0 && strides.head > 0 && strides.sequence > 0 && strides.dimension == 1;
43+
}
44+
45+
} // namespace detail
46+
47+
// Single-token float32 GQA for native KV-head [B, H, S, D] cache views.
48+
// QK and softmax preserve C10's complete per-query-head traversal. P@V groups
49+
// the query heads sharing one KV head so each V token is reused while hot, but
50+
// every output head still accumulates in increasing key-index order.
51+
// Scratch: group_size * kv_sequence floats.
52+
inline bool fwdBhsdFp32(int32_t batch_size, int32_t query_heads, int32_t kv_heads, int32_t kv_sequence, int32_t qk_dim,
53+
int32_t value_dim, const mllm_fp32_t* query, BhsdStrides query_strides, const mllm_fp32_t* key,
54+
BhsdStrides key_strides, const mllm_fp32_t* value, BhsdStrides value_strides, mllm_fp32_t* output,
55+
BhsdStrides output_strides, mllm_fp32_t* grouped_scratch) {
56+
if (batch_size <= 0 || query_heads <= 0 || kv_heads <= 0 || kv_sequence <= 0 || qk_dim <= 0 || value_dim <= 0
57+
|| query_heads % kv_heads != 0 || query == nullptr || key == nullptr || value == nullptr || output == nullptr
58+
|| grouped_scratch == nullptr || !detail::validStrides(query_strides) || !detail::validStrides(key_strides)
59+
|| !detail::validStrides(value_strides) || !detail::validStrides(output_strides)) {
60+
return false;
61+
}
62+
63+
using ArchTag = detail::attention_arch_tag;
64+
const int32_t group_size = query_heads / kv_heads;
65+
const float scale = 1.0F / std::sqrt(static_cast<float>(qk_dim));
66+
67+
for (int32_t batch = 0; batch < batch_size; ++batch) {
68+
for (int32_t kv_head = 0; kv_head < kv_heads; ++kv_head) {
69+
const int32_t first_query_head = kv_head * group_size;
70+
const auto* query_group = query + static_cast<size_t>(batch) * query_strides.batch
71+
+ static_cast<size_t>(first_query_head) * query_strides.head;
72+
const auto* key_head = key + static_cast<size_t>(batch) * key_strides.batch
73+
+ static_cast<size_t>(kv_head) * key_strides.head;
74+
const auto* value_head = value + static_cast<size_t>(batch) * value_strides.batch
75+
+ static_cast<size_t>(kv_head) * value_strides.head;
76+
auto* output_group = output + static_cast<size_t>(batch) * output_strides.batch
77+
+ static_cast<size_t>(first_query_head) * output_strides.head;
78+
79+
for (int32_t group_index = 0; group_index < group_size; ++group_index) {
80+
const auto* query_token = query_group + static_cast<size_t>(group_index) * query_strides.head;
81+
auto* probabilities = grouped_scratch + static_cast<size_t>(group_index) * kv_sequence;
82+
float maximum = std::numeric_limits<float>::lowest();
83+
for (int32_t key_index = 0; key_index < kv_sequence; ++key_index) {
84+
const auto* key_token = key_head + static_cast<size_t>(key_index) * key_strides.sequence;
85+
float score = 0.0F;
86+
flash_attn2::details::VectorDotProduct<ArchTag, mllm_fp32_t, mllm_fp32_t, mllm_fp32_t>::run(
87+
query_token, key_token, &score, static_cast<size_t>(qk_dim));
88+
probabilities[key_index] = score * scale;
89+
maximum = std::max(maximum, probabilities[key_index]);
90+
}
91+
92+
float denominator = 0.0F;
93+
for (int32_t key_index = 0; key_index < kv_sequence; ++key_index) {
94+
probabilities[key_index] = std::exp(probabilities[key_index] - maximum);
95+
denominator += probabilities[key_index];
96+
}
97+
const float inverse_denominator = 1.0F / denominator;
98+
for (int32_t key_index = 0; key_index < kv_sequence; ++key_index) {
99+
probabilities[key_index] *= inverse_denominator;
100+
}
101+
}
102+
103+
for (int32_t group_index = 0; group_index < group_size; ++group_index) {
104+
auto* output_token = output_group + static_cast<size_t>(group_index) * output_strides.head;
105+
flash_attn2::details::FilledWithConst<ArchTag, mllm_fp32_t>::run(output_token, 0.0F,
106+
static_cast<size_t>(value_dim));
107+
}
108+
for (int32_t key_index = 0; key_index < kv_sequence; ++key_index) {
109+
const auto* value_token = value_head + static_cast<size_t>(key_index) * value_strides.sequence;
110+
auto* probability = grouped_scratch + key_index;
111+
auto* output_token = output_group;
112+
for (int32_t group_index = 0; group_index < group_size; ++group_index) {
113+
flash_attn2::details::FMAConstArray<ArchTag, mllm_fp32_t, mllm_fp32_t, mllm_fp32_t>::run(
114+
output_token, *probability, value_token, static_cast<size_t>(value_dim));
115+
probability += kv_sequence;
116+
output_token += output_strides.head;
117+
}
118+
}
119+
}
120+
}
121+
return true;
122+
}
123+
124+
} // namespace mllm::cpu::gqa_decode
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) MLLM Team.
2+
// Licensed under the MIT License.
3+
4+
#include "mllm/backends/cpu/ops/GroupedQueryAttentionDecodeOp.hpp"
5+
6+
#include <algorithm>
7+
#include <cmath>
8+
#include <limits>
9+
#include <vector>
10+
11+
#include "mllm/backends/cpu/kernels/common/gqa_decode/fwd_bhsd.hpp"
12+
13+
namespace mllm::cpu {
14+
namespace {
15+
16+
void groupedQueryAttentionDecodeFloat32Reference(const Tensor& query, const Tensor& key, const Tensor& value, Tensor& output) {
17+
const auto q_shape = query.shape();
18+
const auto k_shape = key.shape();
19+
const auto v_shape = value.shape();
20+
const auto q_stride = query.stride();
21+
const auto k_stride = key.stride();
22+
const auto v_stride = value.stride();
23+
const int32_t groups = q_shape[1] / k_shape[1];
24+
const float scale = 1.0F / std::sqrt(static_cast<float>(q_shape[3]));
25+
26+
static thread_local std::vector<float> probabilities;
27+
probabilities.resize(static_cast<size_t>(k_shape[2]));
28+
29+
for (int32_t batch = 0; batch < q_shape[0]; ++batch) {
30+
for (int32_t query_head = 0; query_head < q_shape[1]; ++query_head) {
31+
const int32_t kv_head = query_head / groups;
32+
const auto* q_head = query.coffsettedPtr<float>({batch, query_head, 0, 0});
33+
const auto* k_head = key.coffsettedPtr<float>({batch, kv_head, 0, 0});
34+
const auto* v_head = value.coffsettedPtr<float>({batch, kv_head, 0, 0});
35+
auto* output_head = output.offsettedPtr<float>({batch, query_head, 0, 0});
36+
37+
// Android release builds use -ffast-math; a finite sentinel keeps stable
38+
// softmax valid under finite-math assumptions.
39+
float maximum = std::numeric_limits<float>::lowest();
40+
for (int32_t key_index = 0; key_index < k_shape[2]; ++key_index) {
41+
const auto* key_token = k_head + static_cast<size_t>(key_index) * k_stride[2];
42+
float score = 0.0F;
43+
for (int32_t dim = 0; dim < q_shape[3]; ++dim) {
44+
score += q_head[static_cast<size_t>(dim) * q_stride[3]] * key_token[static_cast<size_t>(dim) * k_stride[3]];
45+
}
46+
probabilities[static_cast<size_t>(key_index)] = score * scale;
47+
maximum = std::max(maximum, probabilities[static_cast<size_t>(key_index)]);
48+
}
49+
50+
float denominator = 0.0F;
51+
for (int32_t key_index = 0; key_index < k_shape[2]; ++key_index) {
52+
auto& probability = probabilities[static_cast<size_t>(key_index)];
53+
probability = std::exp(probability - maximum);
54+
denominator += probability;
55+
}
56+
const float inverse_denominator = 1.0F / denominator;
57+
for (int32_t key_index = 0; key_index < k_shape[2]; ++key_index) {
58+
probabilities[static_cast<size_t>(key_index)] *= inverse_denominator;
59+
}
60+
61+
for (int32_t value_dim = 0; value_dim < v_shape[3]; ++value_dim) {
62+
float result = 0.0F;
63+
for (int32_t key_index = 0; key_index < k_shape[2]; ++key_index) {
64+
const auto* value_token = v_head + static_cast<size_t>(key_index) * v_stride[2];
65+
result += probabilities[static_cast<size_t>(key_index)] * value_token[static_cast<size_t>(value_dim) * v_stride[3]];
66+
}
67+
output_head[value_dim] = result;
68+
}
69+
}
70+
}
71+
}
72+
73+
} // namespace
74+
75+
CPUGroupedQueryAttentionDecodeOp::CPUGroupedQueryAttentionDecodeOp(const aops::GroupedQueryAttentionDecodeOpOptions& options)
76+
: aops::GroupedQueryAttentionDecodeOp(options) {}
77+
78+
void CPUGroupedQueryAttentionDecodeOp::forward(const std::vector<Tensor>& inputs, std::vector<Tensor>& outputs) {
79+
const auto& query = inputs[0];
80+
const auto& key = inputs[1];
81+
const auto& value = inputs[2];
82+
auto& output = outputs[0];
83+
const auto q_shape = query.shape();
84+
const auto k_shape = key.shape();
85+
const auto v_shape = value.shape();
86+
const auto q_stride = query.stride();
87+
const auto k_stride = key.stride();
88+
const auto v_stride = value.stride();
89+
const auto output_stride = output.stride();
90+
91+
static thread_local std::vector<float> probabilities;
92+
const int32_t group_size = q_shape[1] / k_shape[1];
93+
probabilities.resize(static_cast<size_t>(group_size) * k_shape[2]);
94+
95+
const bool completed = cpu::gqa_decode::fwdBhsdFp32(
96+
q_shape[0], q_shape[1], k_shape[1], k_shape[2], q_shape[3], v_shape[3], query.ptr<float>(),
97+
{q_stride[0], q_stride[1], q_stride[2], q_stride[3]}, key.ptr<float>(),
98+
{k_stride[0], k_stride[1], k_stride[2], k_stride[3]}, value.ptr<float>(),
99+
{v_stride[0], v_stride[1], v_stride[2], v_stride[3]}, output.ptr<float>(),
100+
{output_stride[0], output_stride[1], output_stride[2], output_stride[3]}, probabilities.data());
101+
if (!completed) { groupedQueryAttentionDecodeFloat32Reference(query, key, value, output); }
102+
}
103+
104+
} // namespace mllm::cpu
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) MLLM Team.
2+
// Licensed under the MIT License.
3+
4+
#pragma once
5+
6+
#include "mllm/core/BaseOp.hpp"
7+
#include "mllm/core/aops/GroupedQueryAttentionDecodeOp.hpp"
8+
9+
namespace mllm::cpu {
10+
11+
class CPUGroupedQueryAttentionDecodeOp final : public aops::GroupedQueryAttentionDecodeOp {
12+
public:
13+
explicit CPUGroupedQueryAttentionDecodeOp(const aops::GroupedQueryAttentionDecodeOpOptions& options);
14+
15+
void forward(const std::vector<Tensor>& inputs, std::vector<Tensor>& outputs) override;
16+
};
17+
18+
class CPUGroupedQueryAttentionDecodeOpFactory
19+
: public TypedOpFactory<OpTypes::kGroupedQueryAttentionDecode, aops::GroupedQueryAttentionDecodeOpOptions> {
20+
protected:
21+
std::shared_ptr<BaseOp> createOpImpl(const aops::GroupedQueryAttentionDecodeOpOptions& options) override {
22+
return std::make_shared<CPUGroupedQueryAttentionDecodeOp>(options);
23+
}
24+
};
25+
26+
} // namespace mllm::cpu

0 commit comments

Comments
 (0)