Fix Conv LHS packing padding/uninitialized ptrs#27214
Fix Conv LHS packing padding/uninitialized ptrs#27214JonathanC-ARM wants to merge 1 commit intomicrosoft:mainfrom
Conversation
Signed-off-by: Jonathan Clohessy <Jonathan.Clohessy@arm.com>
adrianlizarraga
left a comment
There was a problem hiding this comment.
I can also confirm that this patch resolves the non-deterministic output on M4. Thank you!
|
/azp run Linux QNN CI Pipeline, Win_TRT_Minimal_CUDA_Test_CI, Win_TRT_Minimal_CUDA_Test_CI, Windows GPU Doc Gen CI Pipeline |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
/azp run Windows ARM64 QNN CI Pipeline |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
thanks for the fix. are there existing unit tests which would catch this or do we need more test coverage? |
I was planning on investigating this and following on with a subsequent pr to address it. I suspect no tests cover this exactly is the answer though, we routinely run the unit and integration tests with this code path and we haven't seen any accuracy issues like this before. |
|
|
||
| // pad_ptr must be at least 'ci' floats for padding pixels. | ||
| // Using a thread_local grow-only buffer to avoid cross-thread interference and ensure sizing is correct. | ||
| thread_local std::vector<float> pad_ptr; |
There was a problem hiding this comment.
So each thread would have its own individual instance of this vector, and no synchronization mechanism is required because the storage is thread-local and not shared between threads.
sounds good. I'm fine with a subsequent PR for it due to the urgency. |
There was a problem hiding this comment.
Pull request overview
This PR attempts to fix non-deterministic corruption in CPU Conv operations when using the MLAS KleidiAI implementation. It addresses two issues:
Changes:
- Fully initialize the LHS indirection table (
lhs_ptrs) to prevent uninitialized reads by the packing kernel - Replace the static fixed-size padding buffer with a thread-local grow-only buffer to handle varying channel counts
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| thread_local std::vector<float> pad_ptr; | ||
| if (pad_ptr.size() < padsize) { | ||
| pad_ptr.resize(padsize, 0.f); | ||
| } else { | ||
| // Ensure any previously-used region remains zeroed (grow-only means it should already be zeros, | ||
| // but keep this explicit for safety). | ||
| std::fill(pad_ptr.begin(), pad_ptr.end(), 0.f); | ||
| } |
There was a problem hiding this comment.
The cached lhs_ptrs array contains pointers to pad_ptr (line 403, and via pixel_offset lambda at lines 420, 425, 430). When pad_ptr is resized (line 473), its underlying storage may be reallocated to a new address, invalidating all pointers stored in previously cached lhs_ptrs entries.
Example scenario:
- Call with ci=100, padsize=256: pad_ptr at address A, lhs_ptrs cached with pointers to A
- Call with ci=500, padsize=512: pad_ptr resized to address B
- Call with ci=100 again: cache hit returns lhs_ptrs with stale pointers to A (now invalid)
The old static vector never moved after first initialization (though it had the undersizing bug this PR fixes). The thread_local grow-only approach fixes the sizing issue but breaks the caching because the cache doesn't invalidate when pad_ptr moves.
Possible solutions:
- Invalidate lhs_ptrs_cache when pad_ptr is resized
- Store pad_ptr address in cache key to detect when it has moved
- Use a stable allocation for pad_ptr that doesn't move when it grows
- Don't cache lhs_ptrs, or recalculate pointers before use
There was a problem hiding this comment.
I think Copilot has a point about potential hanging on to pointers that may be invalidated due to the resize() logic
There was a problem hiding this comment.
I don't think this comment is accurate. The lhs_ptrs array is created every time LhsPtrFill is called. LhsPtrFill always receives the updated pad_ptr after it has been resized. Please correct me if I'm mistaken @JonathanC-ARM .
Update: It seems maybe this is a problem
There was a problem hiding this comment.
I think adding pad_ptr to the cache key might be the way to go (Coplilot's second bullet)
### Description Refer to V1 of the fix here: #27214 This PR includes all fixes from the V1 PR + logic to invalidate the lhs cache pointers in case the pad buffer's underlying buffer has changed due to a resize. The ARM team will look at potentially enhancing this logic after the 1.24.0 release. ### Motivation and Context Fix #26669
### Description Refer to V1 of the fix here: #27214 This PR includes all fixes from the V1 PR + logic to invalidate the lhs cache pointers in case the pad buffer's underlying buffer has changed due to a resize. The ARM team will look at potentially enhancing this logic after the 1.24.0 release. ### Motivation and Context Fix #26669
|
can be closed since #27215 is merged, right? 👍 |
|
Closing this as "V2" of the fix has been merged |
|
Apologies, forgot to close this on my end! |
No problem :) |
### Description This pr introduces some minor code changes which do the following: - Fix copilot header include suggestions from #27439 - Add testcase which covers code path fixed via #27215 and test case discussed in #27214 - Change pointer invalidation to cover only updated pointer in pad structure ### Testing This patch was tested in two ways. 1) After creating tests which I thought would trigger a previous failure case I reverted the convolve_kleidiai.cpp code to before the initial fix in [Hari's change](#27215) for pad ptr was introduced. Added debug logging and tested for failures to highlight the moving and invalidation of pointer. Example failure below 2) I reintroduced the current code and then tested multiple times <br> `for i in $(seq 1 2000); do echo "ITER=$i"; ./onnxruntime_mlas_test --long --gtest_filter='*Conv2d*' || break; done` ### Explanation of Subsequent logs <br> 1) **Padding buffer relocation** - `KLEIDIAI_CONV_LHS pad_buf MOVED ci=320 padsize=512 old=0x12e80d800 new=0x12e81ac00` - Meaning: the internal zero padding buffer used for out-of-bounds pixels was resized and the underlying storage address changed (`old` → `new`). Any previously-built indirection table entries that pointed at the old padding buffer are now stale. 2) **Reuse of cached indirection table after the move** - `KLEIDIAI_CONV_LHS indirection_cache HIT ci=64 m=121 **pad=0x12e81ac00 old_pad=0x12e80d800 (after_pad_move)**` - Meaning: for a later convolution (`ci=64`) the indirection-table cache returned a HIT. The log prints the current pad buffer address (`pad=...`) and the most recent prior padding-buffer address (`old_pad=...`) captured during the move. The `(after_pad_move)` tag indicates that this cache HIT occurred after a pad-buffer relocation event, which is the dangerous case in the pre-fix implementation (cached tables may still contain pointers to `old_pad`). In failing runs, the output mismatch occurs immediately after this sequence, showing a clear correlation: **pad buffer moved → cached indirection table reused → incorrect results**. * one note for the test is I commented out most of the rest of the fixture in the changed file before running for time constraints on the 2000 runs ``` jonclo01$ ./onnxruntime_mlas_test --long --gtest_filter='*Conv2d*' clear ------------------------------------------------------- ----Total 3066 tests registered programmably! ------------------------------------------------------- Note: Google Test filter = *Conv2d* [==========] Running 2 tests from 2 test suites. [----------] Global test environment set-up. [----------] 1 test from Conv2d_SingleThread [ RUN ] Conv2d_SingleThread.LongExecute [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 496 : KLEIDIAI_CONV_LHS pad_buf ci=64 padsize=256 addr=0x12e80d800 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 543 : KLEIDIAI_CONV_LHS indirection_cache MISS ci=64 m=121 pad=0x12e80d800 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 325 : kai_run_lhs_imatmul_pack_x32p2vlx1_x32p_sme M=121 k_chunk_count=9 k_chunk_length=64 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 376 : kai_run_rhs_imatmul_pack_kxn_x32p2vlx1b_x32_x32_sme N=32 k_chunk_count=9 k_chunk_length=64 rhs_stride_row=128 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 653 : kai_run_imatmul_clamp_f32_f32p2vlx1_f32p2vlx1b_2vlx2vl_sme2_mopa M=121 N=32 k_chunk_count=9 k_chunk_length=64 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/sgemm_kleidiai.cpp : 349 : kai_run_rhs_pack_kxn_f32p2vlx1biasf32_f32_f32_sme Groups=1 N=121 K=576 nr=32 kr=1 sr=1 rhs_stride_row=484 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 490 : KLEIDIAI_CONV_LHS **pad_buf MOVED ci=320 padsize=512 old=0x12e80d800 new=0x12e81ac00** [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 543 : KLEIDIAI_CONV_LHS indirection_cache MISS ci=320 m=121 pad=0x12e81ac00 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 325 : kai_run_lhs_imatmul_pack_x32p2vlx1_x32p_sme M=121 k_chunk_count=9 k_chunk_length=320 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 376 : kai_run_rhs_imatmul_pack_kxn_x32p2vlx1b_x32_x32_sme N=32 k_chunk_count=9 k_chunk_length=320 rhs_stride_row=128 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 653 : kai_run_imatmul_clamp_f32_f32p2vlx1_f32p2vlx1b_2vlx2vl_sme2_mopa M=121 N=32 k_chunk_count=9 k_chunk_length=320 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/sgemm_kleidiai.cpp : 349 : kai_run_rhs_pack_kxn_f32p2vlx1biasf32_f32_f32_sme Groups=1 N=121 K=2880 nr=32 kr=1 sr=1 rhs_stride_row=484 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 535 : KLEIDIAI_CONV_LHS indirection_cache HIT ci=64 m=121 **pad=0x12e81ac00 old_pad=0x12e80d800 (after_pad_move)** [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 325 : kai_run_lhs_imatmul_pack_x32p2vlx1_x32p_sme M=121 k_chunk_count=9 k_chunk_length=64 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cpp : 653 : kai_run_imatmul_clamp_f32_f32p2vlx1_f32p2vlx1b_2vlx2vl_sme2_mopa M=121 N=32 k_chunk_count=9 k_chunk_length=64 [KLEIDIAI KERNEL]: /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/core/mlas/lib/kleidiai/sgemm_kleidiai.cpp : 349 : kai_run_rhs_pack_kxn_f32p2vlx1biasf32_f32_f32_sme Groups=1 N=121 K=576 nr=32 kr=1 sr=1 rhs_stride_row=484 /Users/jonclo01/kfi-devenv/repos/onnxruntime/onnxruntime/test/mlas/unittest/test_conv2d.h:249: Failure Expected equality of these values: memcmp(Output, OutputReference, OutputElements * sizeof(float)) Which is: 90 0 B1/G1/Cpg64/Fpg32/H11/W11/KH3/KW3/Pad1,1,1,1/Dilation1,1/Stride1,1 Stack trace: 0x10247ba34: MlasConv2DTest<>::ExecuteLong() 0x102651904: testing::internal::HandleExceptionsInMethodIfSupported<>() 0x1026517a4: testing::Test::Run() 0x102652b5c: testing::TestInfo::Run() 0x102653c84: testing::TestSuite::Run() ... Google Test internal frames ... [ FAILED ] Conv2d_SingleThread.LongExecute, where GetParam() = LongExecute (10 ms) ``` --------- Signed-off-by: Jonathan Clohessy <Jonathan.Clohessy@arm.com>
Description
This PR fixes non-deterministic corruption on CPU EP observed when the CPU Conv path is dispatched through the MLAS KleidiAI implementation.
The fix is entirely within
onnxruntime/core/mlas/lib/kleidiai/convolve_kleidiai.cppand addresses two correctness issues in the KleidiAI Conv LHS packing/padding logic:lhs_ptrs) for partial tiles (m < m_step) so the LHS packing kernel never reads uninitialized entries.ci, preventing out-of-bounds reads for later convolutions with larger channel counts.Motivation and Context
This change is required to fix incorrect, non-deterministic Conv outputs on CPU (often extreme magnitudes /
-FLT_MAX-like values) after running certain Conv-heavy models in the same process. It fixes the root causes of uninitialized reads in the KleidiAI Conv LHS packing path and out-of-bounds reads from an undersized padding buffer.Fixes: #26669