Skip to content

Commit bd0cb9a

Browse files
martin-klacer-armJonathanC-ARMdamdoo01-arm
authored
[MLAS] KleidiAI fix igemm regression (#28571)
### Description <!-- Describe your changes. --> This PR fixes a convolution performance regression affecting some OCR models with large-kernel convolutions when the KleidiAI SME IGEMM convolution path is selected. The change has 2 parts: 1. updates to the KleidiAI IGEMM LHS packing to pack rows in bounded chunks instead of packing the full LHS buffer up front, which reduces memory usage and improves cache locality for large convolutions, 2. a new route selection function `ArmKleidiAI::SelectConvRoute` that decides between `Igemm`, `GemmFallback` and `None` based on convolution parameters and a workload size-based heuristic. The function `CheckCapabilitiesSme` runs `SelectConvRoute` and only returns true if the selected route is `Igemm`. The patch also adds a standard GEMM fallback to the `ConvRoute` possibilities, and runs `MlasGemm` if said fallback is selected. If the function selects `None`, then the convolution falls back to `MlasSgemmOperation`. ### Motivation and Context <!-- - Why is this change required? What problem does it solve? - If it fixes an open issue, please link to the issue here. --> Fixes #27633. --------- Signed-off-by: Qxiang Xu <Qixiang.Xu@arm.com> Signed-off-by: Jonathan Clohessy <Jonathan.Clohessy@arm.com> Signed-off-by: Martin Klacer <martin.klacer@arm.com> Co-authored-by: Jonathan Clohessy <Jonathan.Clohessy@arm.com> Co-authored-by: Damien Dooley <damien.dooley@arm.com>
1 parent 3b77775 commit bd0cb9a

11 files changed

Lines changed: 398 additions & 159 deletions

File tree

include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,14 @@ static const char* const kOrtSessionOptionsMlasLutGemm = "mlas.use_lut_gemm";
498498
// - "1": Disable KleidiAI kernels even if available.
499499
static const char* const kOrtSessionOptionsMlasDisableKleidiAi = "mlas.disable_kleidiai";
500500

501+
// Power-user tuning option for the Arm® KleidiAI™ SME IGEMM convolution route on Arm64.
502+
// For 2D convolutions where both SME IGEMM and the MlasGemm SGEMM fallback are valid routes, work is estimated as:
503+
// output_h * output_w * input_channels * dilated_kernel_h * dilated_kernel_w * filter_count.
504+
// Work above this threshold routes through the SGEMM fallback; work at or below it stays on IGEMM.
505+
// "0" or unset uses the MLAS default heuristic, intended for typical workloads.
506+
// This option exists for perf experimentation; the default may be retuned in future ORT releases.
507+
static const char* const kOrtSessionOptionsMlasKleidiAiConvIgemmMaxWork = "mlas.kleidiai.conv_igemm_max_work";
508+
501509
// When converting DQ + MatMul -> MatMulNBits, the accuracy level of the MatMulNBits is controlled by this option.
502510
// Refer to MatMulNBits op schema for more details.
503511
// If not provided, default is 4.

onnxruntime/core/mlas/inc/mlas.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ MlasActivation(
207207

208208
struct MLAS_BACKEND_KERNEL_SELECTOR_CONFIG {
209209
bool use_kleidiai = true; /**< Flag to use KleidiAI backend kernels if available */
210+
size_t kleidiai_conv_igemm_max_work = 0; /**< Optional SME IGEMM route threshold override; 0 uses default */
210211
};
211212

212213
//

onnxruntime/core/mlas/lib/convolve.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,9 @@ Return Value:
574574
const size_t FilterCount = Parameters->FilterCount;
575575
const size_t OutputSize = Parameters->OutputSize;
576576
const size_t K = Parameters->K;
577+
const bool use_gemm_batch_override =
578+
GetMlasPlatform().MlasConvSGemmRouteOverride != nullptr &&
579+
GetMlasPlatform().MlasConvSGemmRouteOverride(Parameters) == MlasConvSGemmRouteDispatch;
577580

578581
//
579582
// Compute the strides to step through slices of the local segment.
@@ -637,9 +640,15 @@ Return Value:
637640
SegmentStartN + n, CountN);
638641
}
639642

640-
MlasSgemmOperation(CblasNoTrans, CblasNoTrans, FilterCount, CountN,
641-
CountK, 1.0f, Filter + k, K, ColumnBuffer, CountN, beta,
642-
SegmentOutput, OutputSize);
643+
if (use_gemm_batch_override) {
644+
MlasGemm(CblasNoTrans, CblasNoTrans, FilterCount, CountN,
645+
CountK, 1.0f, Filter + k, K, ColumnBuffer, CountN, beta,
646+
SegmentOutput, OutputSize, nullptr, Parameters->BackendKernelSelectorConfig);
647+
} else {
648+
MlasSgemmOperation(CblasNoTrans, CblasNoTrans, FilterCount, CountN,
649+
CountK, 1.0f, Filter + k, K, ColumnBuffer, CountN, beta,
650+
SegmentOutput, OutputSize);
651+
}
643652

644653
beta = 1.0f;
645654
}
@@ -1385,7 +1394,12 @@ MlasConvSupportsDenseChannelsLast2DFloatKernel(
13851394
return false;
13861395
}
13871396

1388-
if (Padding[0] != Padding[2] || Padding[1] != Padding[3]) {
1397+
// The channels-last float convolution path is only implemented by the Arm® KleidiAI™
1398+
// SME conv override, which currently uses a single padding value in its LHS indirection table.
1399+
// Require uniform padding until separate H/W padding is supported there.
1400+
if (Padding[0] != Padding[2] ||
1401+
Padding[1] != Padding[3] ||
1402+
Padding[0] != Padding[1]) {
13891403
return false;
13901404
}
13911405

onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp

Lines changed: 135 additions & 115 deletions
Large diffs are not rendered by default.

onnxruntime/core/mlas/lib/kleidiai/mlasi_kleidiai.h

Lines changed: 151 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
#pragma once
88

99
#include "../mlasi.h"
10-
#include <iostream>
1110

1211
// Fix to ensure compatibility with MSVC build
1312
#if defined(_MSC_VER)
@@ -25,6 +24,7 @@
2524
#endif
2625

2726
#if KLEIDIAI_DEBUG_LOGGING ||KLEIDIAI_KERNEL_LOGGING
27+
#include <iostream>
2828
#define KLEIDIAI_LOG(tag, msg) \
2929
do { \
3030
std::cout << "[KLEIDIAI " << tag << "]: " << __FILE__ << " : " << __LINE__ << " : " << msg << std::endl; \
@@ -56,6 +56,143 @@ inline const bool UseSME2 = MLAS_CPUIDINFO::GetCPUIDInfo().HasArm_SME2();
5656
inline const bool UseSME = MLAS_CPUIDINFO::GetCPUIDInfo().HasArm_SME();
5757
inline const std::string_view vendor_name = MLAS_CPUIDINFO::GetCPUIDInfo().GetCPUVendor();
5858

59+
// Selects the convolution route for Arm® KleidiAI™
60+
enum class ConvRoute {
61+
NoKleidiAi, // decline the conv, caller runs unchanged
62+
IGemm, // handle the whole conv via SME IGEMM kernel
63+
SGemmFallback, // decline IGEMM, but still route the per-segment SGEMM slices through MlasGemm
64+
// so that the Arm® KleidiAI™ SGEMM backend override can pick them up
65+
};
66+
67+
struct ConvRouteSelection {
68+
ConvRoute route = ConvRoute::NoKleidiAi;
69+
size_t effective_kernel_h = 0;
70+
size_t effective_kernel_w = 0;
71+
};
72+
73+
// Heuristic default for SME IGEMM selection. Work is estimated as
74+
// output_m * effective_k * filter_count; larger workloads use the SGEMM-backed
75+
// fallback to avoid IGEMM packing overhead on large effective-k convolutions.
76+
inline constexpr size_t ConvIgemmMaxWorkDefault = 1'000'000ULL;
77+
78+
inline bool TryComputeDilatedKernelSize(size_t dilation, size_t kernel, size_t* result) {
79+
if (dilation == 0 || kernel == 0) return false;
80+
81+
// using formula: dilated_kernel_size = dilation * (kernel - 1) + 1
82+
size_t scaled_kernel;
83+
if (MlasMultiplyOverflowsSizeT(kernel - 1, dilation, &scaled_kernel)) return false;
84+
85+
if (scaled_kernel == SIZE_MAX) return false;
86+
87+
*result = scaled_kernel + 1;
88+
return true;
89+
}
90+
91+
inline bool TryComputeConvOutputSize(size_t input, size_t kernel, size_t padding, size_t stride, size_t* result) {
92+
93+
if (stride == 0) return false;
94+
95+
size_t double_padding;
96+
if (MlasMultiplyOverflowsSizeT(padding, 2, &double_padding)) return false;
97+
98+
if (double_padding > (SIZE_MAX - input)) return false;
99+
const size_t padded_input = double_padding + input;
100+
101+
if (padded_input < kernel) return false;
102+
103+
// using formula: output_size = ((2*padding + input - kernel) / stride) + 1
104+
const size_t output_minus_one = (padded_input - kernel) / stride;
105+
if (output_minus_one == SIZE_MAX) return false;
106+
*result = output_minus_one + 1;
107+
return true;
108+
}
109+
110+
inline ConvRouteSelection SelectConvRoute(const MLAS_CONV_PARAMETERS* Parameters) {
111+
if ((Parameters->Dimensions != 2) ||
112+
(Parameters->BatchCount != 1) ||
113+
(Parameters->Beta != 0.f) ||
114+
(Parameters->Padding[0] != Parameters->Padding[1]) ||
115+
(Parameters->Padding[0] != Parameters->Padding[2]) ||
116+
(Parameters->Padding[0] != Parameters->Padding[3])) {
117+
return ConvRouteSelection{};
118+
}
119+
120+
size_t effective_kernel_h;
121+
size_t effective_kernel_w;
122+
if (!TryComputeDilatedKernelSize(Parameters->DilationShape[0], Parameters->KernelShape[0], &effective_kernel_h) ||
123+
!TryComputeDilatedKernelSize(Parameters->DilationShape[1], Parameters->KernelShape[1], &effective_kernel_w)) {
124+
return ConvRouteSelection{};
125+
}
126+
127+
size_t output_m;
128+
size_t output_h_size;
129+
size_t output_w_size;
130+
if (!TryComputeConvOutputSize(Parameters->InputShape[0],
131+
effective_kernel_h,
132+
Parameters->Padding[0],
133+
Parameters->StrideShape[0],
134+
&output_h_size) ||
135+
!TryComputeConvOutputSize(Parameters->InputShape[1],
136+
effective_kernel_w,
137+
Parameters->Padding[1],
138+
Parameters->StrideShape[1],
139+
&output_w_size) ||
140+
MlasMultiplyOverflowsSizeT(output_h_size, output_w_size, &output_m)) {
141+
return ConvRouteSelection{};
142+
}
143+
144+
if (output_m == 0) {
145+
return ConvRouteSelection{};
146+
}
147+
148+
if (Parameters->KernelShape[0] < 3 || Parameters->KernelShape[1] < 3) {
149+
return ConvRouteSelection{};
150+
}
151+
152+
const auto filter_count = Parameters->FilterCount;
153+
if (filter_count == 0) {
154+
return ConvRouteSelection{};
155+
}
156+
157+
size_t effective_k;
158+
if (MlasMultiplyOverflowsSizeT(Parameters->InputChannels, effective_kernel_h, &effective_k) ||
159+
MlasMultiplyOverflowsSizeT(effective_k, effective_kernel_w, &effective_k)) {
160+
return ConvRouteSelection{};
161+
}
162+
if (effective_k == 0) {
163+
return ConvRouteSelection{};
164+
}
165+
166+
// Currently, the fallback routes assume NCHW layout, so keep valid NHWC convolutions on IGEMM
167+
if (Parameters->ChannelsLast) {
168+
return ConvRouteSelection{ConvRoute::IGemm, effective_kernel_h, effective_kernel_w};
169+
}
170+
171+
if (filter_count == 1) {
172+
// Single-output-channel convolutions do not fill the SME IGEMM N tile efficiently,
173+
// so defer to the default convolution route.
174+
return ConvRouteSelection{};
175+
}
176+
177+
size_t total_work;
178+
if (MlasMultiplyOverflowsSizeT(output_m, effective_k, &total_work) ||
179+
MlasMultiplyOverflowsSizeT(total_work, filter_count, &total_work)) {
180+
// Total work calculation overflow means total work is too large, fall back
181+
return ConvRouteSelection{ConvRoute::SGemmFallback, effective_kernel_h, effective_kernel_w};
182+
}
183+
184+
const size_t conv_igemm_max_work =
185+
Parameters->BackendKernelSelectorConfig != nullptr &&
186+
Parameters->BackendKernelSelectorConfig->kleidiai_conv_igemm_max_work != 0
187+
? Parameters->BackendKernelSelectorConfig->kleidiai_conv_igemm_max_work
188+
: ConvIgemmMaxWorkDefault;
189+
if (total_work > conv_igemm_max_work) {
190+
return ConvRouteSelection{ConvRoute::SGemmFallback, effective_kernel_h, effective_kernel_w};
191+
}
192+
193+
return ConvRouteSelection{ConvRoute::IGemm, effective_kernel_h, effective_kernel_w};
194+
}
195+
59196
// Buffer packing routines.
60197
//
61198
size_t
@@ -224,38 +361,18 @@ MlasConvSymmetricChannelsLast2DFloatPackW(
224361
size_t PackedFilterGroupStride,
225362
MLAS_THREADPOOL* ThreadPool
226363
);
227-
}
228-
229-
/*++
230-
231-
Routine Description:
232-
233-
This routine determines if a wraparound will occur when multiplying two size_t variables
234-
Uses __builtin_mul_overflow if available on the current system and if not falls back
235-
to a default implementation to check this wraparound.
236-
237-
Arguments:
238-
239-
a - Supplies the first number to be muliplied.
240364

241-
b - Supplies the second number to be muliplied.
242-
243-
out - pointer to a size_t which acts as the return value in success cases.
244-
245-
Return Value:
246-
247-
Returns false if the operation was successful
248-
Returns true if wraparound of size_t was detected
249-
250-
--*/
251-
inline bool mul_overflow_size_t_builtin(size_t a, size_t b, size_t* out) {
252-
#if defined(__has_builtin)
253-
# if __has_builtin(__builtin_mul_overflow)
254-
return __builtin_mul_overflow(a, b, out);
255-
# endif
256-
#endif
257-
// Fallback to manual check if builtin not available
258-
if (b != 0 && a > SIZE_MAX / b) return true;
259-
if (out) *out = a * b;
260-
return false;
365+
inline MLAS_CONV_SGEMM_ROUTE
366+
MLASCALL
367+
MlasConvSGemmRoute(const MLAS_CONV_PARAMETERS* Parameters) {
368+
if (Parameters->BackendKernelSelectorConfig &&
369+
!Parameters->BackendKernelSelectorConfig->use_kleidiai) {
370+
return MlasConvSGemmRouteDirect;
371+
}
372+
373+
return ArmKleidiAI::SelectConvRoute(Parameters).route == ArmKleidiAI::ConvRoute::SGemmFallback
374+
? MlasConvSGemmRouteDispatch
375+
: MlasConvSGemmRouteDirect;
261376
}
377+
}
378+

onnxruntime/core/mlas/lib/kleidiai/sbgemm_kleidiai.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ Return Value:
279279
LhsPackedStride = kai_get_lhs_packed_size_lhs_pack_bf16p2vlx2_f32_sme(M, K, mr, kr, sr);
280280

281281
size_t lhs_resize = 0;
282-
if (mul_overflow_size_t_builtin(LhsPackedStride, BatchSize, &lhs_resize))
282+
if (MlasMultiplyOverflowsSizeT(LhsPackedStride, BatchSize, &lhs_resize))
283283
{
284284
// size_t wraparound detected for LhsPackedStride, fallback to MLAS
285285
return false;
@@ -304,7 +304,7 @@ Return Value:
304304
// Multithread pack lhs and rhs
305305
RhsPackedStride = ArmKleidiAI::MlasSBGemmPackBSize(TransA, TransB, N, K);
306306
size_t rhs_resize = 0;
307-
if (mul_overflow_size_t_builtin(RhsPackedStride, BatchSize, &rhs_resize))
307+
if (MlasMultiplyOverflowsSizeT(RhsPackedStride, BatchSize, &rhs_resize))
308308
{
309309
// size_t wraparound detected for RhsPackedStride, fallback to MLAS
310310
return false;
@@ -354,7 +354,7 @@ Return Value:
354354
// Pre-check maximum tile size to avoid per-iteration overflow inside the parallel loop.
355355
// Any TileSizeM/TileSizeN used below will be <= m_step/n_step respectively.
356356
size_t max_tile_elems = 0;
357-
if (mul_overflow_size_t_builtin(m_step, n_step, &max_tile_elems)) {
357+
if (MlasMultiplyOverflowsSizeT(m_step, n_step, &max_tile_elems)) {
358358
// size_t wraparound detected for tile size, fallback to MLAS
359359
return false;
360360
}

onnxruntime/core/mlas/lib/kleidiai/sgemm_kleidiai.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ Return Value:
542542
LhsPackedStride = kai_get_lhs_packed_size_lhs_pack_f32p2vlx1_f32_sme(M, K, mr, kr, sr);
543543

544544
size_t lhs_resize = 0;
545-
if(mul_overflow_size_t_builtin(LhsPackedStride, BatchSize, &lhs_resize))
545+
if(MlasMultiplyOverflowsSizeT(LhsPackedStride, BatchSize, &lhs_resize))
546546
{
547547
// size_t wraparound detected for LhsPackedStride, fallback to MLAS
548548
return false;
@@ -568,7 +568,7 @@ Return Value:
568568
// Multithread pack lhs and rhs
569569
RhsPackedStride = ArmKleidiAI::MlasGemmPackBSize(TransA, TransB, N, K);
570570
size_t rhs_resize = 0;
571-
if (mul_overflow_size_t_builtin(RhsPackedStride, BatchSize, &rhs_resize))
571+
if (MlasMultiplyOverflowsSizeT(RhsPackedStride, BatchSize, &rhs_resize))
572572
{
573573
// size_t wraparound detected for RhsPackedStride, fallback to MLAS
574574
return false;
@@ -616,7 +616,7 @@ Return Value:
616616
// Pre-check maximum tile size to avoid per-iteration overflow inside the parallel loop.
617617
// Any TileSizeM/TileSizeN used below will be <= m_step/n_step respectively.
618618
size_t max_tile_elems = 0;
619-
if (mul_overflow_size_t_builtin(m_step, n_step, &max_tile_elems)) {
619+
if (MlasMultiplyOverflowsSizeT(m_step, n_step, &max_tile_elems)) {
620620
// size_t wraparound detected for tile size, fallback to MLAS
621621
return false;
622622
}

onnxruntime/core/mlas/lib/mlasi.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,33 @@ Module Name:
119119

120120
#define MLAS_UNREFERENCED_PARAMETER(parameter) ((void)(parameter))
121121

122+
//
123+
// Reports whether multiplying two size_t values would overflow.
124+
//
125+
126+
MLAS_FORCEINLINE
127+
bool
128+
MlasMultiplyOverflowsSizeT(
129+
size_t a,
130+
size_t b,
131+
size_t* out
132+
)
133+
{
134+
#if defined(__has_builtin)
135+
#if __has_builtin(__builtin_mul_overflow)
136+
size_t result;
137+
return __builtin_mul_overflow(a, b, out != nullptr ? out : &result);
138+
#endif
139+
#endif
140+
if (b != 0 && a > std::numeric_limits<size_t>::max() / b) {
141+
return true;
142+
}
143+
if (out != nullptr) {
144+
*out = a * b;
145+
}
146+
return false;
147+
}
148+
122149
#ifdef MLAS_NO_EXCEPTION
123150

124151
MLAS_FORCEINLINE void
@@ -891,6 +918,18 @@ bool
891918
MLAS_THREADPOOL* ThreadPool
892919
);
893920

921+
enum MLAS_CONV_SGEMM_ROUTE {
922+
MlasConvSGemmRouteDirect, // call MlasSgemmOperation directly
923+
MlasConvSGemmRouteDispatch, // call MlasGemm so backend SGEMM overrides may be selected
924+
};
925+
926+
typedef
927+
MLAS_CONV_SGEMM_ROUTE
928+
(MLASCALL MLAS_CONV_SGEMM_ROUTE_OVERRIDE)(
929+
const MLAS_CONV_PARAMETERS* Parameters
930+
);
931+
932+
894933
typedef
895934
bool
896935
(MLASCALL MLAS_SGEMM_BATCH_OVERRIDE)(
@@ -1520,6 +1559,7 @@ struct MLAS_PLATFORM {
15201559
// MLAS Conv overrides
15211560
MLAS_CONV_PREPARE_FLOAT_OVERRIDE* MlasConvPrepareOverride = nullptr;
15221561
MLAS_CONV_FLOAT_OVERRIDE* MlasConvOverride = nullptr;
1562+
MLAS_CONV_SGEMM_ROUTE_OVERRIDE* MlasConvSGemmRouteOverride = nullptr;
15231563
#if defined(__aarch64__) && defined(__linux__)
15241564
// SBGemm overrides
15251565
MLAS_SBGEMM_BATCH_OVERRIDE* MlasSBGemmBatchOverride = nullptr;

0 commit comments

Comments
 (0)