Skip to content

Commit eb63cc6

Browse files
authored
[MLAS] Fix scratch buffer over-allocation in batched/grouped NCHW Conv (microsoft#28482)
### Description microsoft#26103 which was shipped with ORT 1.23.2 introduced logic to parallelize on the batch and/or group count parameters for NCHW Conv if either were > 1 to improve performance of batched grouped convolutions - prior to that the parallelization was only on the output spatial map. The per-thread scratch buffer allocated by the B/G parallelization path introduced in microsoft#26103 is excessive and is not needed for the algorithm being used. Under the hood, `MlasConvOperation()` uses a double tiled approach which means that the per-thread scratch buffer needs just enough memory to fit that tile of data and nothing more than that. This change fixes the conservative over-allocation which shows up as a memory bloat in OP's model in issue microsoft#28466 Note: the work partitioning across (batch × group) introduced in microsoft#26103 is unchanged — only the per-thread scratch buffer sizing is corrected. The batched/grouped Conv speedup from microsoft#26103 is retained. ### Motivation and Context Fix microsoft#28466
1 parent 45c0663 commit eb63cc6

3 files changed

Lines changed: 133 additions & 5 deletions

File tree

onnxruntime/core/mlas/lib/convolve.cpp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,7 @@ Return Value:
849849
if (bias != nullptr) {
850850
bias += group * FilterCount;
851851
}
852-
float* ColumnBuffer = WorkBlock->WorkingBuffer + Index * OutputSize * K;
852+
float* ColumnBuffer = WorkBlock->WorkingBuffer + Index * MLAS_CONV_WORKING_BUFFER_SIZE_PER_THREAD;
853853

854854
MlasConvOperation(Parameters, input, filter, bias, ColumnBuffer, output, 0, OutputSize);
855855
}
@@ -1712,14 +1712,11 @@ Return Value:
17121712

17131713
if (Parameters->BatchCount > 1 || Parameters->GroupCount > 1) {
17141714

1715-
size_t WorkingBufferSizePerThread = std::max({Parameters->OutputSize * Parameters->K,
1716-
Parameters->FilterCount * Parameters->OutputSize,
1717-
static_cast<size_t>(MLAS_CONV_WORKING_BUFFER_SIZE_PER_THREAD)});
17181715
TargetThreadCount = MaximumThreadCount;
17191716
if (static_cast<size_t>(TargetThreadCount) >= Parameters->BatchCount * Parameters->GroupCount) {
17201717
TargetThreadCount = static_cast<ptrdiff_t>(Parameters->BatchCount * Parameters->GroupCount);
17211718
}
1722-
*WorkingBufferSize = TargetThreadCount * WorkingBufferSizePerThread;
1719+
*WorkingBufferSize = TargetThreadCount * MLAS_CONV_WORKING_BUFFER_SIZE_PER_THREAD;
17231720
}
17241721
}
17251722
}

onnxruntime/test/contrib_ops/fused_conv_test.cc

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,90 @@ TEST(FusedConvTest, Cpu_NhwcConv2D_AutoPadSameUpper) {
374374
}
375375
#endif
376376

377+
TEST(FusedConvTest, Cpu_Conv3D_Batched_Relu) {
378+
constexpr size_t batch_count = 4;
379+
constexpr size_t input_channels = 1;
380+
constexpr size_t input_depth = 8;
381+
constexpr size_t input_height = 8;
382+
constexpr size_t input_width = 8;
383+
constexpr size_t filter_count = 6;
384+
constexpr size_t kernel_depth = 7;
385+
constexpr size_t kernel_height = 7;
386+
constexpr size_t kernel_width = 7;
387+
388+
OpTester test("FusedConv", 1, onnxruntime::kMSDomain);
389+
test.AddAttribute("group", static_cast<int64_t>(1));
390+
test.AddAttribute("kernel_shape", vector<int64_t>{7, 7, 7});
391+
test.AddAttribute("pads", vector<int64_t>{3, 3, 3, 3, 3, 3});
392+
test.AddAttribute("strides", vector<int64_t>{1, 1, 1});
393+
test.AddAttribute("dilations", vector<int64_t>{1, 1, 1});
394+
test.AddAttribute("activation", string("Relu"));
395+
396+
const vector<int64_t> X_shape = {static_cast<int64_t>(batch_count),
397+
static_cast<int64_t>(input_channels),
398+
static_cast<int64_t>(input_depth),
399+
static_cast<int64_t>(input_height),
400+
static_cast<int64_t>(input_width)};
401+
const vector<int64_t> W_shape = {static_cast<int64_t>(filter_count),
402+
static_cast<int64_t>(input_channels),
403+
static_cast<int64_t>(kernel_depth),
404+
static_cast<int64_t>(kernel_height),
405+
static_cast<int64_t>(kernel_width)};
406+
const vector<int64_t> Y_shape = {static_cast<int64_t>(batch_count),
407+
static_cast<int64_t>(filter_count),
408+
static_cast<int64_t>(input_depth),
409+
static_cast<int64_t>(input_height),
410+
static_cast<int64_t>(input_width)};
411+
412+
vector<float> X(batch_count * input_channels * input_depth * input_height * input_width, 1.0f);
413+
vector<float> W(filter_count * input_channels * kernel_depth * kernel_height * kernel_width, 1.0f);
414+
415+
// With X = 1, W = 1, no bias, and a single input channel, the pre-activation output at
416+
// [b][f][d][h][w] equals the number of valid kernel positions that fall inside the input
417+
// volume at (d, h, w). For a kernel of size K with stride 1 and pad = K/2, the per-axis
418+
// valid count at position p in a dimension of length L is:
419+
// count(p, L, K) = min(K - 1, L - 1 - p + K/2) - max(0, K/2 - p) + 1.
420+
// The post-Relu output is the product of the per-axis counts (all values are positive).
421+
auto valid_count = [](int64_t pos, int64_t dim, int64_t kernel) -> int64_t {
422+
const int64_t pad = kernel / 2;
423+
const int64_t lo = std::max<int64_t>(0, pad - pos);
424+
const int64_t hi = std::min<int64_t>(kernel - 1, dim - 1 - pos + pad);
425+
return hi - lo + 1;
426+
};
427+
428+
vector<float> Y(batch_count * filter_count * input_depth * input_height * input_width);
429+
for (size_t b = 0; b < batch_count; ++b) {
430+
for (size_t f = 0; f < filter_count; ++f) {
431+
for (size_t d = 0; d < input_depth; ++d) {
432+
const int64_t cd = valid_count(static_cast<int64_t>(d),
433+
static_cast<int64_t>(input_depth),
434+
static_cast<int64_t>(kernel_depth));
435+
for (size_t h = 0; h < input_height; ++h) {
436+
const int64_t ch = valid_count(static_cast<int64_t>(h),
437+
static_cast<int64_t>(input_height),
438+
static_cast<int64_t>(kernel_height));
439+
for (size_t w = 0; w < input_width; ++w) {
440+
const int64_t cw = valid_count(static_cast<int64_t>(w),
441+
static_cast<int64_t>(input_width),
442+
static_cast<int64_t>(kernel_width));
443+
const size_t idx = ((b * filter_count + f) * input_depth + d) * input_height * input_width +
444+
h * input_width + w;
445+
Y[idx] = static_cast<float>(cd * ch * cw);
446+
}
447+
}
448+
}
449+
}
450+
}
451+
452+
test.AddInput<float>("X", X_shape, X);
453+
test.AddInput<float>("W", W_shape, W, true);
454+
test.AddOutput<float>("Y", Y_shape, Y);
455+
456+
std::vector<std::unique_ptr<IExecutionProvider>> execution_providers;
457+
execution_providers.push_back(DefaultCpuExecutionProvider());
458+
test.Run(OpTester::ExpectResult::kExpectSuccess, "", {}, nullptr, &execution_providers);
459+
}
460+
377461
#endif
378462

379463
} // namespace test

onnxruntime/test/mlas/unittest/test_conv2d.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,52 @@ class MlasConv2DTest : public MlasTestBase {
492492
}
493493
}
494494

495+
void TestBatchedConv3DWorkingBufferUsesThreadTileSize() {
496+
constexpr size_t Dimensions = 3;
497+
constexpr size_t BatchCount = 12;
498+
constexpr size_t GroupCount = 1;
499+
constexpr size_t InputChannels = 1;
500+
constexpr size_t FilterCount = 6;
501+
int64_t InputShape[] = {32, 64, 64};
502+
int64_t KernelShape[] = {7, 7, 7};
503+
int64_t DilationShape[] = {1, 1, 1};
504+
int64_t Padding[] = {3, 3, 3, 3, 3, 3};
505+
int64_t StrideShape[] = {1, 1, 1};
506+
int64_t OutputShape[] = {32, 64, 64};
507+
508+
MLAS_ACTIVATION Activation;
509+
Activation.ActivationKind = MlasIdentityActivation;
510+
511+
MLAS_CONV_PARAMETERS Parameters;
512+
size_t WorkingBufferSize = 0;
513+
514+
MlasConvPrepare(&Parameters,
515+
Dimensions,
516+
BatchCount,
517+
GroupCount,
518+
InputChannels,
519+
InputShape,
520+
KernelShape,
521+
DilationShape,
522+
Padding,
523+
StrideShape,
524+
OutputShape,
525+
FilterCount,
526+
&Activation,
527+
&WorkingBufferSize,
528+
false,
529+
0.0f,
530+
threadpool_);
531+
532+
if (Parameters.Algorithm != MlasConvAlgorithmExpandThenGemmSegmented) {
533+
GTEST_SKIP() << "This platform uses a different Conv3D algorithm.";
534+
}
535+
536+
const size_t full_column_buffer_size = Parameters.OutputSize * Parameters.K;
537+
ASSERT_LT(WorkingBufferSize, full_column_buffer_size)
538+
<< "Batched Conv3D should not allocate a full im2col buffer per worker.";
539+
}
540+
495541
void Test(
496542
size_t BatchCount,
497543
size_t GroupCount,
@@ -688,5 +734,6 @@ class MlasConv2DTest : public MlasTestBase {
688734
TestMobileClipBetaActivationRegression(64, 64, 64);
689735
TestMobileClipBetaActivationRegression(128, 32, 32);
690736
TestMobileClipBetaActivationRegression(256, 16, 16);
737+
TestBatchedConv3DWorkingBufferUsesThreadTileSize();
691738
}
692739
};

0 commit comments

Comments
 (0)