Skip to content

Commit 5aa921d

Browse files
committed
feat: enable QASYMM8_SIGNED to F32 assembly dequantization
Enable QASYMM8_SIGNED input and weights to use the F32 DequantizeFloat assembly output stage, including the direct convolution selection path. Propagate input and weight zero-points plus the unrounded mathematical K depth so asymmetric offset correction uses the real GEMM/convolution depth rather than arm_gemm's padded internal K. Fix the interleaved no-merge DequantizeFloat scheduler stride so kernels that do not pack row-sum slots advance between A panels correctly. Guard the symmetric no-merge dequant support helper with the SME/SME2 feature macros. The helper is only referenced when those no-merge dequantized kernels are compiled, so non-SME builds must not define it unconditionally under Werror. Add NEON validation coverage for the direct I8S8F32 convolution path. Performance was checked on a Cortex-A76, pinned to CPU 4 with one thread. The change is neutral on large workloads and improves the small NHWC signed int8 to F32 convolution path: QASYMM8_SIGNED/RunSmallDequantizeF32 NHWC/no-activation cases show a 1.36x geomean speedup over github/main, with the tiniest single-batch cases improving by roughly 1.45x to 3.13x. Validate GEMM3D convolution probes with the real weights and output tensor metadata instead of reusing the input data type for every dummy tensor. This prevents QASYMM8 input with QSYMM8_PER_CHANNEL weights from selecting a 3D path that the real lowp operation cannot safely configure. When QASYMM8 output is handled through the signed lowp assembly path, route fused assembly through the signed intermediate output, keep the adjusted output stage in sync for configure/validate/runtime quantization updates, and convert back to QASYMM8 after the fused assembly path as well as the fallback path. This fixes quantized per-channel GEMM convolution and deconvolution validation crashes introduced by admitting the int8 per-channel RHS assembly path. Signed-off-by: Pablo Marquez Tello <pablo.tello@arm.com> Change-Id: I945c24e5cd3d21d857b68de90d27aa18a31f7547
1 parent a386845 commit 5aa921d

17 files changed

Lines changed: 951 additions & 88 deletions

File tree

src/core/NEON/kernels/arm_gemm/gemm_interleaved.hpp

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,33 @@ class kernel_and_merge {
7272
Tab *acc_buff);
7373
};
7474

75+
template<typename strategy, typename Tlo, typename Tro, typename Tr, typename Tab>
76+
auto run_dequantized_integrated_kernel(
77+
strategy &strat, const Tlo *a_ptr, const Tro *b_panel, Tr *c_ptr, int ldc,
78+
unsigned int m_size, unsigned int n_size, int kern_k, const int32_t *offset_col_bias,
79+
const DequantizeFloat &dq, const Tr *offset_bias, const Activation &act, bool accumulate,
80+
Tab *acc_buff, const int32_t *row_sum, int)
81+
-> decltype(strat.kernel(a_ptr, b_panel, c_ptr, ldc, m_size, n_size, kern_k,
82+
offset_col_bias, dq, offset_bias, act, accumulate, acc_buff,
83+
row_sum, kern_k),
84+
void())
85+
{
86+
strat.kernel(a_ptr, b_panel, c_ptr, ldc, m_size, n_size, kern_k,
87+
offset_col_bias, dq, offset_bias, act, accumulate, acc_buff,
88+
row_sum, kern_k);
89+
}
90+
91+
template<typename strategy, typename Tlo, typename Tro, typename Tr, typename Tab>
92+
void run_dequantized_integrated_kernel(
93+
strategy &strat, const Tlo *a_ptr, const Tro *b_panel, Tr *c_ptr, int ldc,
94+
unsigned int m_size, unsigned int n_size, int kern_k, const int32_t *offset_col_bias,
95+
const DequantizeFloat &dq, const Tr *offset_bias, const Activation &act, bool accumulate,
96+
Tab *acc_buff, const int32_t *, long)
97+
{
98+
strat.kernel(a_ptr, b_panel, c_ptr, ldc, m_size, n_size, kern_k,
99+
offset_col_bias, dq, offset_bias, act, accumulate, acc_buff);
100+
}
101+
75102
// Run a kernel and call the separate merge step
76103
template<>
77104
template<typename strategy, typename Tlo, typename Tro, typename Tr, typename Tri, typename Tab>
@@ -281,14 +308,19 @@ void kernel_and_merge<false, false, DequantizeFloat>::run(
281308
offset_bias = bias + n_0;
282309
}
283310

284-
strat.kernel(// A and B pointers are just the packed panels.
285-
a_ptr, b_panel,
286-
// Provide relevant part of output array and row stride.
287-
c_ptr ? (c_ptr + m_0 * ldc + n_0) : nullptr, ldc,
288-
// M, N, K sizes
289-
m_max-m_0, n_max - n_0, kern_k,
290-
// Bias, activation, accumulation. Need to offset the bias as needed.
291-
offset_col_bias, dq, offset_bias, act, accumulate, acc_buff);
311+
// When b_offset != 0, row sums of A are packed at the end of the A panel
312+
// (appended by the quantized PrepareA transform with multiplier=1). Read them
313+
// to pass to dequantize_block_32 for per-row offset correction.
314+
const int32_t *row_sum = nullptr;
315+
if (dq.b_offset != 0) {
316+
row_sum = reinterpret_cast<const int32_t *>(a_ptr + strategy::out_height() * kern_k);
317+
}
318+
319+
run_dequantized_integrated_kernel(
320+
strat, a_ptr, b_panel,
321+
c_ptr ? (c_ptr + m_0 * ldc + n_0) : nullptr, ldc,
322+
m_max - m_0, n_max - n_0, kern_k, offset_col_bias, dq, offset_bias, act,
323+
accumulate, acc_buff, row_sum, 0);
292324
}
293325

294326
template<>
@@ -300,7 +332,7 @@ void kernel_and_merge<true, false, DequantizeFloat>::run(
300332
strategy &strat, const Tlo *a_ptr, const Tro *b_panel, size_t, Tri *c_panel,
301333
Tr *c_ptr, int ldc, int kern_k, unsigned int m_0,
302334
unsigned int m_max, unsigned int n_0, unsigned int n_max, const Tr *bias,
303-
const Activation &act, bool not_first_pass, const DequantizeFloat &qp, const int32_t *,
335+
const Activation &act, bool not_first_pass, const DequantizeFloat &qp, const int32_t *col_bias,
304336
Tab *)
305337
{
306338
const int bblocks = iceildiv(n_max - n_0, strategy::out_width());
@@ -317,14 +349,20 @@ void kernel_and_merge<true, false, DequantizeFloat>::run(
317349
#ifdef CYCLE_PROFILING
318350
auto p=prof.ScopedProfiler(PROFILE_QUANTIZE, ((m_max-m_0) * bblocks * strategy::out_width() * sizeof(Tr)));
319351
#endif
352+
// When b_offset != 0, row sums are packed after the A panel data
353+
const int32_t *row_sum = (qp.b_offset != 0)
354+
? reinterpret_cast<const int32_t *>(a_ptr + strategy::out_height() * kern_k)
355+
: nullptr;
356+
320357
for (int i=0; i<bblocks; i++) {
321358
unsigned int n_start = n_0 + (strategy::out_width() * i);
322359
unsigned int n_end = std::min(n_start + strategy::out_width(), n_max);
323360

324361
dequantize_block_32(qp, (n_end - n_start), (m_max - m_0),
325362
c_panel + (i * strategy::out_width() * strategy::out_height()), strategy::out_width(),
326363
c_ptr + m_0 * ldc + n_start, ldc,
327-
bias != nullptr ? bias + n_start : nullptr, not_first_pass, act);
364+
bias != nullptr ? bias + n_start : nullptr, not_first_pass, act,
365+
col_bias != nullptr ? col_bias + n_start : nullptr, row_sum, kern_k);
328366

329367
}
330368
}
@@ -475,6 +513,13 @@ class GemmInterleaved : public GemmCommon<Tlo, Tro, Tr> {
475513
return _Nsize * _nmulti * sizeof(int32_t);
476514
}
477515

516+
if (std::is_same<OutputStage, DequantizeFloat>::value) {
517+
const DequantizeFloat *dq = reinterpret_cast<const DequantizeFloat *>(&_os);
518+
if (dq->a_offset != 0) {
519+
return _Nsize * _nmulti * sizeof(int32_t);
520+
}
521+
}
522+
478523
return 0;
479524
}
480525

@@ -557,6 +602,12 @@ class GemmInterleaved : public GemmCommon<Tlo, Tro, Tr> {
557602
k_depth += sizeof(int32_t) / sizeof(Tloi);
558603
}
559604

605+
if (std::is_same<OutputStage, DequantizeFloat>::value && MergeStep) {
606+
// transforms_quantized always packs row sum slots (zeros when multiplier=0, actual
607+
// sums when b_offset != 0). Reserve space unconditionally when MergeStep is enabled.
608+
k_depth += sizeof(int32_t) / sizeof(Tloi);
609+
}
610+
560611
return k_depth;
561612
}
562613

@@ -647,6 +698,13 @@ class GemmInterleaved : public GemmCommon<Tlo, Tro, Tr> {
647698
return -qp->b_offset;
648699
}
649700

701+
if (std::is_same<OutputStage, DequantizeFloat>::value) {
702+
const DequantizeFloat *dq = reinterpret_cast<const DequantizeFloat *>(&_os);
703+
// Pack row sums into the A panel when b_offset is non-zero so that the
704+
// merge step can apply the b_offset correction per output position.
705+
return (dq->b_offset != 0) ? 1 : 0;
706+
}
707+
650708
return 0;
651709
}
652710

@@ -693,6 +751,14 @@ class GemmInterleaved : public GemmCommon<Tlo, Tro, Tr> {
693751
return get_ktotal(args);
694752
}
695753

754+
// K blocking is not supported for DequantizeFloat with MergeStep when b_offset != 0,
755+
// because row sums of A must cover the full K depth. We cannot check b_offset here
756+
// (static function), so we conservatively disable K-blocking for all DequantizeFloat
757+
// MergeStep cases. The working-memory cost is minimal and correctness is guaranteed.
758+
if (std::is_same<OutputStage, DequantizeFloat>::value && MergeStep) {
759+
return get_ktotal(args);
760+
}
761+
696762
// We can't K block non-fast FP16 cases without an accumulation buffer.
697763
#if defined(__aarch64__) && (defined(FP16_KERNELS) || defined(ARM_COMPUTE_ENABLE_FP16))
698764
if (std::is_same<Tlo, __fp16>::value && std::is_same<Tr, __fp16>::value && !args._fast_mode && MergeStep) {
@@ -937,7 +1003,7 @@ class GemmInterleaved : public GemmCommon<Tlo, Tro, Tr> {
9371003
#endif
9381004
// See comment above on transform_type<> class: this extracts either 'transforms' or
9391005
// 'transforms_quantized' as appropriate.
940-
typename transform_type<strategy, MergeStep && std::is_same<OutputStage, Requantize32>::value>::type transforms;
1006+
typename transform_type<strategy, MergeStep && (std::is_same<OutputStage, Requantize32>::value || std::is_same<OutputStage, DequantizeFloat>::value)>::type transforms;
9411007

9421008
if (_indirect_buf != nullptr) {
9431009
transforms.PrepareA_indirect(a_panel,
@@ -1027,7 +1093,7 @@ class GemmInterleaved : public GemmCommon<Tlo, Tro, Tr> {
10271093
#endif
10281094
// See comment above on transform_type<> class: this extracts either 'transforms' or
10291095
// 'transforms_quantized' as appropriate.
1030-
typename transform_type<strategy, MergeStep && std::is_same<OutputStage, Requantize32>::value>::type transforms;
1096+
typename transform_type<strategy, MergeStep && (std::is_same<OutputStage, Requantize32>::value || std::is_same<OutputStage, DequantizeFloat>::value)>::type transforms;
10311097

10321098
for (unsigned int batch = batch_0; batch <= batch_end; batch++) {
10331099
unsigned int first_m = (batch == batch_0) ? m_0 : 0;
@@ -1060,6 +1126,10 @@ class GemmInterleaved : public GemmCommon<Tlo, Tro, Tr> {
10601126

10611127
if(std::is_same<OutputStage, Requantize32>::value) {
10621128
a_panel_stride = kern_k + (sizeof(int32_t) / sizeof(Tloi));
1129+
} else if (std::is_same<OutputStage, DequantizeFloat>::value && MergeStep) {
1130+
// transforms_quantized always packs row-sum slots (zeros when b_offset=0,
1131+
// actual sums when b_offset != 0), so the stride must include the slot.
1132+
a_panel_stride = kern_k + (sizeof(int32_t) / sizeof(Tloi));
10631133
} else {
10641134
a_panel_stride = kern_k;
10651135
}
@@ -1212,6 +1282,20 @@ class GemmInterleaved : public GemmCommon<Tlo, Tro, Tr> {
12121282
compute_col_sums(*qp_ptr, _Nsize, _Ksize * _Ksections, B + (i * B_multi_stride), ldb, col_bias + (i * _Nsize), _Ksize * _Ksections, i, 0);
12131283
}
12141284
}
1285+
1286+
if (std::is_same<OutputStage, DequantizeFloat>::value) {
1287+
const DequantizeFloat *dq = reinterpret_cast<const DequantizeFloat *>(&_os);
1288+
if (dq->a_offset != 0) {
1289+
// Compute raw column sums of B (weight matrix) for use in a_offset correction.
1290+
// dequantize_block_32 applies: -a_offset * col_sums[n] * scale per output channel.
1291+
col_bias = reinterpret_cast<int32_t *>(in_buffer);
1292+
for (unsigned int i = 0; i < _nmulti; ++i) {
1293+
compute_raw_col_sums(_Nsize, _Ksize * _Ksections,
1294+
B + (i * B_multi_stride), ldb,
1295+
col_bias + (i * _Nsize));
1296+
}
1297+
}
1298+
}
12151299
}
12161300

12171301
// Support for transposed B is a property of the strategy::transpose type
@@ -1431,4 +1515,3 @@ template<typename strategy, typename Tlo, typename Tro, typename Tr>
14311515
using GemmInterleavedDequantized = GemmInterleaved<strategy, Tlo, Tro, Tr, DequantizeFloat>;
14321516

14331517
} // namespace arm_gemm
1434-

src/core/NEON/kernels/arm_gemm/gemm_s8fp32.cpp

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,48 +49,77 @@
4949

5050
namespace arm_gemm {
5151

52+
#if defined(ARM_COMPUTE_ENABLE_SME2) || defined(ARM_COMPUTE_ENABLE_SME)
53+
namespace {
54+
55+
bool supports_symmetric_dequant_no_merge(const GemmArgs &args, const DequantizeFloat &dq)
56+
{
57+
return !args._accumulate && dq.a_offset == 0 && dq.b_offset == 0;
58+
}
59+
60+
} // namespace
61+
#endif // defined(ARM_COMPUTE_ENABLE_SME2) || defined(ARM_COMPUTE_ENABLE_SME)
62+
5263
static const GemmImplementation<int8_t, int8_t, float, DequantizeFloat> gemm_s8fp32_methods[] =
5364
{
5465
#ifdef ARM_COMPUTE_ENABLE_SME2
5566
{
5667
"sme2_interleaved_nomerge_s8qfp32_mopa_1VLx4VL",
57-
[](const GemmArgs &args, const DequantizeFloat &) { return args._ci->has_sme2() && args._ci->has_sme_i8i32() && !args._accumulate; },
68+
[](const GemmArgs &args, const DequantizeFloat &dq) {
69+
return args._ci->has_sme2() && args._ci->has_sme_i8i32() &&
70+
supports_symmetric_dequant_no_merge(args, dq);
71+
},
5872
[](const GemmArgs &args, const DequantizeFloat &) { const auto VL = sme::get_vector_length<float>();
5973
return args._Msize <= VL || (2*VL < args._Msize && args._Msize <= 3*VL); },
6074
[](const GemmArgs &args, const DequantizeFloat &dq) { return new GemmInterleavedNoMergeDequantized<cls_sme2_interleaved_nomerge_s8qfp32_mopa_1VLx4VL, int8_t, int8_t, float>(args, dq); }
6175
},
6276
{
6377
"sme2_interleaved_nomerge_s8qfp32_mopa_4VLx1VL",
64-
[](const GemmArgs &args, const DequantizeFloat &) { return args._ci->has_sme2() && args._ci->has_sme_i8i32() && !args._accumulate; },
78+
[](const GemmArgs &args, const DequantizeFloat &dq) {
79+
return args._ci->has_sme2() && args._ci->has_sme_i8i32() &&
80+
supports_symmetric_dequant_no_merge(args, dq);
81+
},
6582
[](const GemmArgs &args, const DequantizeFloat &) { const auto VL = sme::get_vector_length<float>();
6683
return args._Nsize <= VL || (2*VL < args._Nsize && args._Nsize <= 3*VL); },
6784
[](const GemmArgs &args, const DequantizeFloat &dq) { return new GemmInterleavedNoMergeDequantized<cls_sme2_interleaved_nomerge_s8qfp32_mopa_4VLx1VL, int8_t, int8_t, float>(args, dq); }
6885
},
6986
{
7087
"sme2_interleaved_nomerge_s8qfp32_mopa_2VLx2VL",
71-
[](const GemmArgs &args, const DequantizeFloat &) { return args._ci->has_sme2() && args._ci->has_sme_i8i32() && !args._accumulate; },
88+
[](const GemmArgs &args, const DequantizeFloat &dq) {
89+
return args._ci->has_sme2() && args._ci->has_sme_i8i32() &&
90+
supports_symmetric_dequant_no_merge(args, dq);
91+
},
7292
nullptr,
7393
[](const GemmArgs &args, const DequantizeFloat &dq) { return new GemmInterleavedNoMergeDequantized<cls_sme2_interleaved_nomerge_s8qfp32_mopa_2VLx2VL, int8_t, int8_t, float>(args, dq); }
7494
},
7595
#endif // ARM_COMPUTE_ENABLE_SME2
7696
#ifdef ARM_COMPUTE_ENABLE_SME
7797
{
7898
"sme_interleaved_nomerge_s8qfp32_mopa_1VLx4VL",
79-
[](const GemmArgs &args, const DequantizeFloat &) { return args._ci->has_sme() && args._ci->has_sme_i8i32() && !args._accumulate; },
99+
[](const GemmArgs &args, const DequantizeFloat &dq) {
100+
return args._ci->has_sme() && args._ci->has_sme_i8i32() &&
101+
supports_symmetric_dequant_no_merge(args, dq);
102+
},
80103
[](const GemmArgs &args, const DequantizeFloat &) { const auto VL = sme::get_vector_length<float>();
81104
return args._Msize <= VL || (2*VL < args._Msize && args._Msize <= 3*VL); },
82105
[](const GemmArgs &args, const DequantizeFloat &dq) { return new GemmInterleavedNoMergeDequantized<cls_sme_interleaved_nomerge_s8qfp32_mopa_1VLx4VL, int8_t, int8_t, float>(args, dq); }
83106
},
84107
{
85108
"sme_interleaved_nomerge_s8qfp32_mopa_4VLx1VL",
86-
[](const GemmArgs &args, const DequantizeFloat &) { return args._ci->has_sme() && args._ci->has_sme_i8i32() && !args._accumulate; },
109+
[](const GemmArgs &args, const DequantizeFloat &dq) {
110+
return args._ci->has_sme() && args._ci->has_sme_i8i32() &&
111+
supports_symmetric_dequant_no_merge(args, dq);
112+
},
87113
[](const GemmArgs &args, const DequantizeFloat &) { const auto VL = sme::get_vector_length<float>();
88114
return args._Nsize <= VL || (2*VL < args._Nsize && args._Nsize <= 3*VL); },
89115
[](const GemmArgs &args, const DequantizeFloat &dq) { return new GemmInterleavedNoMergeDequantized<cls_sme_interleaved_nomerge_s8qfp32_mopa_4VLx1VL, int8_t, int8_t, float>(args, dq); }
90116
},
91117
{
92118
"sme_interleaved_nomerge_s8qfp32_mopa_2VLx2VL",
93-
[](const GemmArgs &args, const DequantizeFloat &) { return args._ci->has_sme() && args._ci->has_sme_i8i32() && !args._accumulate; },
119+
[](const GemmArgs &args, const DequantizeFloat &dq) {
120+
return args._ci->has_sme() && args._ci->has_sme_i8i32() &&
121+
supports_symmetric_dequant_no_merge(args, dq);
122+
},
94123
nullptr,
95124
[](const GemmArgs &args, const DequantizeFloat &dq) { return new GemmInterleavedNoMergeDequantized<cls_sme_interleaved_nomerge_s8qfp32_mopa_2VLx2VL, int8_t, int8_t, float>(args, dq); }
96125
},
@@ -153,4 +182,3 @@ template std::vector<KernelDescription> get_compatible_kernels<int8_t, int8_t, f
153182
} // namespace arm_gemm
154183

155184
#endif // __aarch64__
156-

src/core/NEON/kernels/arm_gemm/quantized-fp16.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ namespace arm_gemm {
3535
template<>
3636
void dequantize_block_32<__fp16>(const DequantizeFloat &qp, unsigned int width, unsigned int height,
3737
const int32_t * in_ptr, unsigned int in_stride, __fp16 *out_ptr, unsigned int out_stride,
38-
const __fp16 * bias_ptr, bool not_first_pass, const Activation &act)
38+
const __fp16 * bias_ptr, bool not_first_pass, const Activation &act,
39+
const int32_t * /*col_bias*/, const int32_t * /*row_sum*/, int32_t /*k_total*/)
3940
{
4041
const float32x4_t vscale = vdupq_n_f32(qp.scale);
4142
float maxval = std::numeric_limits<float>::infinity();

0 commit comments

Comments
 (0)