-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Add AVX2 LayerNorm/RMSNorm kernel for x86-64 (MLAS) #31973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Justin Chu (justinchuby)
merged 18 commits into
microsoft:main
from
justinchuby:nxrt/mlas-avx2-layernorm
Sep 4, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
07354dd
Add AVX2 LayerNorm/RMSNorm kernel for x86-64
justinchuby 9ed3e78
Add MLAS unit tests for the AVX2 LayerNorm kernel
justinchuby 10f5e75
Apply clang-format to the MLAS LayerNorm unit tests
justinchuby 7a90a4f
Preserve Welford semantics in the AVX2 LayerNorm kernel and skip tiny…
justinchuby b5a8ac1
Skip the unused mean accumulation in the RMSNorm path
justinchuby 86e7759
Replace lane-parallel Welford with centered two-pass; fix cross-platf…
justinchuby f751b5c
Widen sweep tolerance headroom and make the adversarial report runnable
justinchuby 72e02cd
Fix architecture-specific dispatch threshold in LayerNorm tests
justinchuby 697189f
Fix stale comments and add threshold cross-references
justinchuby 9a4fcae
Fix stale algorithm references in LayerNorm test comments
justinchuby a49b702
Guard precision suites with HasCenteredTwoPassKernel() (x86-64 only)
justinchuby 4a16925
Fix comment wording: 'x86-64' → 'x86' to match AMD64/IX86 gate
justinchuby fbf322f
Fix evidence-accuracy rejection: reproducible B1 figures, nullptr Mea…
justinchuby 30ddb33
Merge remote-tracking branch 'origin/main' into nxrt/mlas-avx2-layernorm
justinchuby f6c736c
Complete AVX2 LayerNorm support on x86
justinchuby 63b4e49
Avoid short-row RMSNorm regression
justinchuby 23a4c2c
Fix non-x86 LayerNorm test build
justinchuby 12a570c
Address LayerNorm benchmark review feedback
justinchuby File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| /*++ | ||
|
|
||
| Copyright (c) Microsoft Corporation. All rights reserved. | ||
|
|
||
| Licensed under the MIT License. | ||
|
|
||
| Module Name: | ||
|
|
||
| layernorm_kernel_avx2.cpp | ||
|
|
||
| Abstract: | ||
|
|
||
| This module implements LayerNorm/RMSNorm kernels using x86 AVX2+FMA3 | ||
| intrinsics. Processes one normalization row at a time, matching the | ||
| MLAS_LAYERNORM_F32_KERNEL signature dispatched from platform.cpp. | ||
|
|
||
| RMSNorm uses a vectorised sum-of-squares accumulation (two-pass: | ||
| reduce then normalise), processing 8 floats per iteration. | ||
|
|
||
| Full LayerNorm uses a centered two-pass algorithm: | ||
| Pass 1 — compute the mean via a double-precision sum (4 doubles | ||
| per AVX2 iteration using vcvtps2pd + vaddpd). This keeps | ||
| rounding in the mean from corrupting the centered variance | ||
| for large-magnitude inputs. | ||
| Pass 2 — accumulate sum((x - mean)^2) in fp32 (8 floats per | ||
| iteration). Subtracting the (accurate) mean before | ||
| squaring eliminates the catastrophic cancellation that | ||
| plagues the uncentered E[x^2]-mean^2 formulation. | ||
|
|
||
| A scalar tail handles lengths that are not a multiple of 8 (or 4 | ||
| for the double-precision mean pass). | ||
|
|
||
| --*/ | ||
|
|
||
| #include "mlasi.h" | ||
|
|
||
| #if defined(MLAS_TARGET_AMD64) || defined(MLAS_TARGET_IX86) | ||
|
|
||
| #include <immintrin.h> | ||
|
|
||
| #include <cassert> | ||
| #include <cmath> | ||
|
|
||
| void MLASCALL | ||
| MlasLayerNormKernelAvx2( | ||
| const float* Input, | ||
| const float* Scale, | ||
| const float* Bias, | ||
| float* Output, | ||
| float* MeanOut, | ||
| float* InvStdDevOut, | ||
| size_t NormSize, | ||
| float Epsilon, | ||
| bool Simplified) | ||
| { | ||
| assert(!Simplified || Bias == nullptr); | ||
|
|
||
| const size_t n = NormSize; | ||
|
|
||
| float mean_val; | ||
| float inv_denom; | ||
|
|
||
| if (Simplified) { | ||
| // | ||
| // RMSNorm: accumulate sum-of-squares for the inverse RMS | ||
| // denominator. The mean is only needed when the caller requests | ||
| // MeanOut — the normalisation itself never subtracts the mean. | ||
| // Skip the sum accumulation when MeanOut is null to avoid ~1 | ||
| // extra vaddps per 8-element iteration. | ||
| // | ||
|
|
||
| __m256 vsumsq = _mm256_setzero_ps(); | ||
| size_t i = 0; | ||
| float sumsq_val; | ||
|
|
||
| if (MeanOut != nullptr) { | ||
| // | ||
| // Caller wants the mean: accumulate sum in double precision | ||
| // alongside sum-of-squares in fp32. | ||
| // | ||
| __m256d vsumd = _mm256_setzero_pd(); | ||
| for (; i + 8 <= n; i += 8) { | ||
| __m256 vx = _mm256_loadu_ps(Input + i); | ||
| __m128 vx_lo = _mm256_castps256_ps128(vx); | ||
| __m128 vx_hi = _mm256_extractf128_ps(vx, 1); | ||
| vsumd = _mm256_add_pd(vsumd, _mm256_cvtps_pd(vx_lo)); | ||
| vsumd = _mm256_add_pd(vsumd, _mm256_cvtps_pd(vx_hi)); | ||
| vsumsq = _mm256_fmadd_ps(vx, vx, vsumsq); | ||
| } | ||
|
|
||
| // Horizontal reduce double sum. | ||
| __m128d hi_d = _mm256_extractf128_pd(vsumd, 1); | ||
| __m128d lo_d = _mm256_castpd256_pd128(vsumd); | ||
| __m128d rd = _mm_add_pd(lo_d, hi_d); | ||
| rd = _mm_add_sd(rd, _mm_unpackhi_pd(rd, rd)); | ||
| double dsum = _mm_cvtsd_f64(rd); | ||
|
|
||
| // Horizontal reduce sum-of-squares. | ||
| __m128 hi_sq = _mm256_extractf128_ps(vsumsq, 1); | ||
| __m128 lo_sq = _mm256_castps256_ps128(vsumsq); | ||
| __m128 r_sq = _mm_add_ps(lo_sq, hi_sq); | ||
| r_sq = _mm_add_ps(r_sq, _mm_movehl_ps(r_sq, r_sq)); | ||
| r_sq = _mm_add_ss(r_sq, _mm_movehdup_ps(r_sq)); | ||
| sumsq_val = _mm_cvtss_f32(r_sq); | ||
|
|
||
| for (; i < n; i++) { | ||
| dsum += static_cast<double>(Input[i]); | ||
| sumsq_val += Input[i] * Input[i]; | ||
| } | ||
|
|
||
| mean_val = static_cast<float>(dsum / static_cast<double>(n)); | ||
| } else { | ||
| // | ||
| // No mean requested: sum-of-squares only. | ||
| // | ||
| for (; i + 8 <= n; i += 8) { | ||
| __m256 vx = _mm256_loadu_ps(Input + i); | ||
| vsumsq = _mm256_fmadd_ps(vx, vx, vsumsq); | ||
| } | ||
|
|
||
| // Horizontal reduce sum-of-squares. | ||
| __m128 hi_sq = _mm256_extractf128_ps(vsumsq, 1); | ||
| __m128 lo_sq = _mm256_castps256_ps128(vsumsq); | ||
| __m128 r_sq = _mm_add_ps(lo_sq, hi_sq); | ||
| r_sq = _mm_add_ps(r_sq, _mm_movehl_ps(r_sq, r_sq)); | ||
| r_sq = _mm_add_ss(r_sq, _mm_movehdup_ps(r_sq)); | ||
| sumsq_val = _mm_cvtss_f32(r_sq); | ||
|
|
||
| for (; i < n; i++) { | ||
| sumsq_val += Input[i] * Input[i]; | ||
| } | ||
|
|
||
| mean_val = 0.0f; | ||
| } | ||
|
|
||
| inv_denom = 1.0f / sqrtf(sumsq_val / static_cast<float>(n) + Epsilon); | ||
| } else { | ||
| // | ||
| // Full LayerNorm: centered two-pass algorithm. | ||
| // | ||
| // Pass 1 — Compute the mean using double-precision accumulation. | ||
| // This prevents fp32 summation error for large-magnitude inputs | ||
| // from corrupting the centered variance in the second pass. | ||
| // | ||
|
|
||
| __m256d vsumd = _mm256_setzero_pd(); | ||
| size_t i = 0; | ||
| for (; i + 4 <= n; i += 4) { | ||
| __m128 vf = _mm_loadu_ps(Input + i); | ||
| vsumd = _mm256_add_pd(vsumd, _mm256_cvtps_pd(vf)); | ||
| } | ||
|
|
||
| // Horizontal reduce the 4 double lanes. | ||
| __m128d hi_d = _mm256_extractf128_pd(vsumd, 1); | ||
| __m128d lo_d = _mm256_castpd256_pd128(vsumd); | ||
| __m128d rd = _mm_add_pd(lo_d, hi_d); | ||
| rd = _mm_add_sd(rd, _mm_unpackhi_pd(rd, rd)); | ||
| double dsum = _mm_cvtsd_f64(rd); | ||
|
|
||
| for (; i < n; i++) { | ||
| dsum += static_cast<double>(Input[i]); | ||
| } | ||
|
|
||
| mean_val = static_cast<float>(dsum / static_cast<double>(n)); | ||
|
|
||
| // | ||
| // Pass 2 — Accumulate centered sum-of-squared-deviations in fp32. | ||
| // Subtracting the (accurate) mean before squaring removes the | ||
| // catastrophic cancellation that plagues E[x^2] - mean^2. | ||
| // | ||
|
|
||
| __m256 vmean_acc = _mm256_set1_ps(mean_val); | ||
| __m256 vvar = _mm256_setzero_ps(); | ||
| i = 0; | ||
| for (; i + 8 <= n; i += 8) { | ||
| __m256 vd = _mm256_sub_ps(_mm256_loadu_ps(Input + i), vmean_acc); | ||
| vvar = _mm256_fmadd_ps(vd, vd, vvar); | ||
| } | ||
|
|
||
| // Horizontal reduce. | ||
| __m128 hi = _mm256_extractf128_ps(vvar, 1); | ||
| __m128 lo = _mm256_castps256_ps128(vvar); | ||
| __m128 r = _mm_add_ps(lo, hi); | ||
| r = _mm_add_ps(r, _mm_movehl_ps(r, r)); | ||
| r = _mm_add_ss(r, _mm_movehdup_ps(r)); | ||
| float var_val = _mm_cvtss_f32(r); | ||
|
|
||
| for (; i < n; i++) { | ||
| float d = Input[i] - mean_val; | ||
| var_val += d * d; | ||
| } | ||
|
|
||
| inv_denom = 1.0f / sqrtf(var_val / static_cast<float>(n) + Epsilon); | ||
| } | ||
|
|
||
| // | ||
| // Pass 2: Normalise and write output. | ||
| // | ||
|
|
||
| __m256 vmean = _mm256_set1_ps(mean_val); | ||
| __m256 vinv = _mm256_set1_ps(inv_denom); | ||
|
|
||
| size_t i = 0; | ||
| if (Simplified) { | ||
| for (; i + 8 <= n; i += 8) { | ||
| __m256 vx = _mm256_loadu_ps(Input + i); | ||
| __m256 vs = _mm256_loadu_ps(Scale + i); | ||
| __m256 vy = _mm256_mul_ps(vx, vinv); | ||
| vy = _mm256_mul_ps(vy, vs); | ||
| _mm256_storeu_ps(Output + i, vy); | ||
| } | ||
| for (; i < n; i++) { | ||
| Output[i] = Input[i] * inv_denom * Scale[i]; | ||
| } | ||
| } else if (Bias == nullptr) { | ||
| for (; i + 8 <= n; i += 8) { | ||
| __m256 vx = _mm256_loadu_ps(Input + i); | ||
| __m256 vs = _mm256_loadu_ps(Scale + i); | ||
| __m256 vy = _mm256_sub_ps(vx, vmean); | ||
| vy = _mm256_mul_ps(vy, vinv); | ||
| vy = _mm256_mul_ps(vy, vs); | ||
| _mm256_storeu_ps(Output + i, vy); | ||
| } | ||
| for (; i < n; i++) { | ||
| Output[i] = (Input[i] - mean_val) * inv_denom * Scale[i]; | ||
| } | ||
| } else { | ||
| for (; i + 8 <= n; i += 8) { | ||
| __m256 vx = _mm256_loadu_ps(Input + i); | ||
| __m256 vs = _mm256_loadu_ps(Scale + i); | ||
| __m256 vb = _mm256_loadu_ps(Bias + i); | ||
| __m256 vy = _mm256_sub_ps(vx, vmean); | ||
| vy = _mm256_mul_ps(vy, vinv); | ||
| vy = _mm256_fmadd_ps(vy, vs, vb); | ||
| _mm256_storeu_ps(Output + i, vy); | ||
| } | ||
| for (; i < n; i++) { | ||
| Output[i] = (Input[i] - mean_val) * inv_denom * Scale[i] + Bias[i]; | ||
| } | ||
| } | ||
|
|
||
| if (MeanOut != nullptr) { | ||
| *MeanOut = mean_val; | ||
| } | ||
| if (InvStdDevOut != nullptr) { | ||
| *InvStdDevOut = inv_denom; | ||
| } | ||
| } | ||
|
|
||
| #endif // MLAS_TARGET_AMD64 || MLAS_TARGET_IX86 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.