Files
TheRock/rocm-libraries/projects/hipdnn/test_sdk/tests/utilities/TestCpuFpReferenceLayernormBackward.cpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

1731 lines
65 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include "hipdnn_data_sdk/utilities/Constants.hpp"
#include <cmath>
#include <gtest/gtest.h>
#include <hipdnn_data_sdk/types.hpp>
#include <hipdnn_data_sdk/utilities/Tensor.hpp>
#include <hipdnn_test_sdk/utilities/CpuFpReferenceLayernorm.hpp>
#include <hipdnn_test_sdk/utilities/TestTolerances.hpp>
#include <hipdnn_test_sdk/utilities/detail/CpuFpReferenceUtilities.hpp>
using namespace hipdnn_test_sdk::utilities;
using namespace hipdnn_data_sdk::utilities;
using namespace hipdnn_data_sdk::types;
using hipdnn_test_sdk::detail::safeTestTypeCast;
// ============================================================================
// Hand-computed golden reference tests
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropSanityValidation2D)
{
// Input shape: [2, 4] — normalize over last dim (4 features per sample)
// dy = [[-1, 2, -3, 4],
// [2, -4, 6, -8]]
// x = [[1, 2, 3, 4],
// [2, 4, 6, 8]]
//
// Sample 0: mean=2.5, var=1.25, rstd=1/sqrt(1.25+1e-5)=0.894423613312618
// a = 1.7888329159619083
// b = -3.5776586765921525
// dx = [-4.7998926705705713e-05, 3.577687297918808, -7.15538175116928, 3.5777159192454633]
//
// Sample 1: mean=5.0, var=5.0, rstd=1/sqrt(5.0+1e-5)=0.4472131482870333
// a = -0.8944245077250512
// b = 3.5776962420511893
// dx = [2.3999731674440028e-05, -3.5777033974472507, 7.155408583743517, -3.577710552843312]
//
// dscale = [-1.341640786446209, 0.8944271909641394, 1.341640786446209, -5.366563145784836]
// dbias = [1, -2, 3, -4]
Tensor<double> dy({2, 4});
Tensor<double> x({2, 4});
Tensor<double> scale({4});
Tensor<double> dx({2, 4});
Tensor<double> dscale({4});
Tensor<double> dbias({4});
dy.setHostValue(-1.0, 0, 0);
dy.setHostValue(2.0, 0, 1);
dy.setHostValue(-3.0, 0, 2);
dy.setHostValue(4.0, 0, 3);
dy.setHostValue(2.0, 1, 0);
dy.setHostValue(-4.0, 1, 1);
dy.setHostValue(6.0, 1, 2);
dy.setHostValue(-8.0, 1, 3);
x.setHostValue(1.0, 0, 0);
x.setHostValue(2.0, 0, 1);
x.setHostValue(3.0, 0, 2);
x.setHostValue(4.0, 0, 3);
x.setHostValue(2.0, 1, 0);
x.setHostValue(4.0, 1, 1);
x.setHostValue(6.0, 1, 2);
x.setHostValue(8.0, 1, 3);
scale.fillWithValue(2.0);
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, nullptr, nullptr, 1);
auto tolerance = layernorm::getTolerance<double>();
// Sample 0 outputs
EXPECT_NEAR(dx.getHostValue(0, 0), -2.1465994991753945e-05, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 1), 3.577687297918808, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 2), -7.15538175116928, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 3), 3.5777159192454633, tolerance);
// Sample 1 outputs
EXPECT_NEAR(dx.getHostValue(1, 0), 5.366547046303793e-06, tolerance);
EXPECT_NEAR(dx.getHostValue(1, 1), -3.5777033974472507, tolerance);
EXPECT_NEAR(dx.getHostValue(1, 2), 7.155408583743517, tolerance);
EXPECT_NEAR(dx.getHostValue(1, 3), -3.577710552843312, tolerance);
// dscale
EXPECT_NEAR(dscale.getHostValue(0), -1.3416434697532726, tolerance);
EXPECT_NEAR(dscale.getHostValue(1), 0.8944289798355152, tolerance);
EXPECT_NEAR(dscale.getHostValue(2), 1.3416434697532726, tolerance);
EXPECT_NEAR(dscale.getHostValue(3), -5.366573879013091, tolerance);
// dbias
EXPECT_NEAR(dbias.getHostValue(0), 1.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(1), -2.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(2), 3.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(3), -4.0, tolerance);
}
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropSanityValidationWithOptional2D)
{
// Input shape: [2, 4] — normalize over last dim (4 features per sample)
// dy = [[-1, 2, -3, 4],
// [2, -4, 6, -8]]
// x = [[1, 2, 3, 4],
// [2, 4, 6, 8]]
//
// Sample 0: mean=2.5, var=1.25, rstd=1/sqrt(1.25+1e-5)=0.894423613312618
// a = 1.7888329159619083
// b = -3.5776586765921525
// dx = [-4.7998926705705713e-05, 3.577687297918808, -7.15538175116928, 3.5777159192454633]
//
// Sample 1: mean=5.0, var=5.0, rstd=1/sqrt(5.0+1e-5)=0.4472131482870333
// a = -0.8944245077250512
// b = 3.5776962420511893
// dx = [2.3999731674440028e-05, -3.5777033974472507, 7.155408583743517, -3.577710552843312]
//
// dscale = [-1.341640786446209, 0.8944271909641394, 1.341640786446209, -5.366563145784836]
// dbias = [1, -2, 3, -4]
Tensor<double> dy({2, 4});
Tensor<double> x({2, 4});
Tensor<double> scale({4});
Tensor<double> mean({2});
Tensor<double> rstd({2});
Tensor<double> dx({2, 4});
Tensor<double> dscale({4});
Tensor<double> dbias({4});
dy.setHostValue(-1.0, 0, 0);
dy.setHostValue(2.0, 0, 1);
dy.setHostValue(-3.0, 0, 2);
dy.setHostValue(4.0, 0, 3);
dy.setHostValue(2.0, 1, 0);
dy.setHostValue(-4.0, 1, 1);
dy.setHostValue(6.0, 1, 2);
dy.setHostValue(-8.0, 1, 3);
x.setHostValue(1.0, 0, 0);
x.setHostValue(2.0, 0, 1);
x.setHostValue(3.0, 0, 2);
x.setHostValue(4.0, 0, 3);
x.setHostValue(2.0, 1, 0);
x.setHostValue(4.0, 1, 1);
x.setHostValue(6.0, 1, 2);
x.setHostValue(8.0, 1, 3);
mean.setHostValue(2.5, 0);
mean.setHostValue(5.0, 1);
rstd.setHostValue(1.0 / std::sqrt(1.25 + LAYERNORM_DEFAULT_EPSILON), 0);
rstd.setHostValue(1.0 / std::sqrt(5.0 + LAYERNORM_DEFAULT_EPSILON), 1);
scale.fillWithValue(2.0);
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
// Sample 0 outputs
EXPECT_NEAR(dx.getHostValue(0, 0), -2.1465994991753945e-05, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 1), 3.577687297918808, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 2), -7.15538175116928, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 3), 3.5777159192454633, tolerance);
// Sample 1 outputs
EXPECT_NEAR(dx.getHostValue(1, 0), 5.366547046303793e-06, tolerance);
EXPECT_NEAR(dx.getHostValue(1, 1), -3.5777033974472507, tolerance);
EXPECT_NEAR(dx.getHostValue(1, 2), 7.155408583743517, tolerance);
EXPECT_NEAR(dx.getHostValue(1, 3), -3.577710552843312, tolerance);
// dscale
EXPECT_NEAR(dscale.getHostValue(0), -1.3416434697532726, tolerance);
EXPECT_NEAR(dscale.getHostValue(1), 0.8944289798355152, tolerance);
EXPECT_NEAR(dscale.getHostValue(2), 1.3416434697532726, tolerance);
EXPECT_NEAR(dscale.getHostValue(3), -5.366573879013091, tolerance);
// dbias
EXPECT_NEAR(dbias.getHostValue(0), 1.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(1), -2.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(2), 3.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(3), -4.0, tolerance);
}
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropSanityValidation3D)
{
// Input shape: [1, 2, 3] — normalize over last 2 dims (2x3 = 6 features)
// dy = [[[-1, 2, -3], [4, -5, 6]]]
// x = [[[1, 2, 3], [4, 5, 6]]]
//
// mean = (1+2+3+4+5+6)/6 = 3.5
// var = ((1-3.5)^2 + (2-3.5)^2 + (3-3.5)^2 + (4-3.5)^2 + (5-3.5)^2 + (6-3.5)^2) / 6
// = (6.25 + 2.25 + 0.25 + 0.25 + 2.25 + 6.25) / 6 = 17.5 / 6 = 2.9166666666666665
// rstd = 1/sqrt(2.9166666666666665 + 1e-5) = 0.5855390399887689
//
// With scale=1, bias=0 (identity affine):
// a = rstd³ * (sum(dy * scale * x) - sum(dy * scale) * mean) / 6
// b = rstd * sum(dy * scale) / 6 - a * mean
// dx = rstd * dy * scale - a * x - b
// dscale = sum(dy * (x - mean) * rstd)
// dbias = sum(dy)
Tensor<double> dy({1, 2, 3});
Tensor<double> x({1, 2, 3});
Tensor<double> scale({2, 3});
Tensor<double> dx({1, 2, 3});
Tensor<double> dscale({2, 3});
Tensor<double> dbias({2, 3});
dy.setHostValue(-1.0, 0, 0, 0);
dy.setHostValue(2.0, 0, 0, 1);
dy.setHostValue(-3.0, 0, 0, 2);
dy.setHostValue(4.0, 0, 1, 0);
dy.setHostValue(-5.0, 0, 1, 1);
dy.setHostValue(6.0, 0, 1, 2);
x.setHostValue(1.0, 0, 0, 0);
x.setHostValue(2.0, 0, 0, 1);
x.setHostValue(3.0, 0, 0, 2);
x.setHostValue(4.0, 0, 1, 0);
x.setHostValue(5.0, 0, 1, 1);
x.setHostValue(6.0, 0, 1, 2);
scale.fillWithValue(1.0);
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, nullptr, nullptr, 2);
Tensor<double> dxRef({1, 2, 3});
Tensor<double> dscaleRef({2, 3});
Tensor<double> dbiasRef({2, 3});
dxRef.setHostValue(-3.0113333097103734e-06, 0, 0, 0);
dxRef.setHostValue(1.4052918891730595, 0, 0, 1);
dxRef.setHostValue(-1.8737255302307223, 0, 0, 2);
dxRef.setHostValue(1.8737255302307223, 0, 1, 0);
dxRef.setHostValue(-3.7474480491281357, 0, 1, 1);
dxRef.setHostValue(2.342159171288385, 0, 1, 2);
dscaleRef.setHostValue(1.4638475999719223, 0, 0);
dscaleRef.setHostValue(-1.7566171199663065, 0, 1);
dscaleRef.setHostValue(0.8783085599831533, 0, 2);
dscaleRef.setHostValue(1.1710780799775378, 1, 0);
dscaleRef.setHostValue(-4.391542799915767, 1, 1);
dscaleRef.setHostValue(8.783085599831534, 1, 2);
dbiasRef.setHostValue(-1.0, 0, 0);
dbiasRef.setHostValue(2.0, 0, 1);
dbiasRef.setHostValue(-3.0, 0, 2);
dbiasRef.setHostValue(4.0, 1, 0);
dbiasRef.setHostValue(-5.0, 1, 1);
dbiasRef.setHostValue(6.0, 1, 2);
auto tolerance = layernorm::getTolerance<double>();
// Verify each output element: y = (x - mean) * rstd
for(int i = 0; i < 2; i++)
{
for(int j = 0; j < 3; j++)
{
EXPECT_NEAR(dx.getHostValue(0, i, j), dxRef.getHostValue(0, i, j), tolerance);
EXPECT_NEAR(dscale.getHostValue(i, j), dscaleRef.getHostValue(i, j), tolerance);
EXPECT_NEAR(dbias.getHostValue(i, j), dbiasRef.getHostValue(i, j), tolerance);
}
}
}
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropSanityValidationWithOptional3D)
{
// Input shape: [1, 2, 3] — normalize over last 2 dims (2x3 = 6 features)
// dy = [[[-1, 2, -3], [4, -5, 6]]]
// x = [[[1, 2, 3], [4, 5, 6]]]
//
// mean = (1+2+3+4+5+6)/6 = 3.5
// var = ((1-3.5)^2 + (2-3.5)^2 + (3-3.5)^2 + (4-3.5)^2 + (5-3.5)^2 + (6-3.5)^2) / 6
// = (6.25 + 2.25 + 0.25 + 0.25 + 2.25 + 6.25) / 6 = 17.5 / 6 = 2.9166666666666665
// rstd = 1/sqrt(2.9166666666666665 + 1e-5) = 0.5855390399887689
//
// With scale=1, bias=0 (identity affine):
// a = rstd³ * (sum(dy * scale * x) - sum(dy * scale) * mean) / 6
// b = rstd * sum(dy * scale) / 6 - a * mean
// dx = rstd * dy * scale - a * x - b
// dscale = sum(dy * (x - mean) * rstd)
// dbias = sum(dy)
Tensor<double> dy({1, 2, 3});
Tensor<double> x({1, 2, 3});
Tensor<double> scale({2, 3});
Tensor<double> mean({1});
Tensor<double> rstd({1});
Tensor<double> dx({1, 2, 3});
Tensor<double> dscale({2, 3});
Tensor<double> dbias({2, 3});
dy.setHostValue(-1.0, 0, 0, 0);
dy.setHostValue(2.0, 0, 0, 1);
dy.setHostValue(-3.0, 0, 0, 2);
dy.setHostValue(4.0, 0, 1, 0);
dy.setHostValue(-5.0, 0, 1, 1);
dy.setHostValue(6.0, 0, 1, 2);
x.setHostValue(1.0, 0, 0, 0);
x.setHostValue(2.0, 0, 0, 1);
x.setHostValue(3.0, 0, 0, 2);
x.setHostValue(4.0, 0, 1, 0);
x.setHostValue(5.0, 0, 1, 1);
x.setHostValue(6.0, 0, 1, 2);
scale.fillWithValue(1.0);
mean.setHostValue(3.5, 0);
rstd.setHostValue(0.5855390399887689, 0);
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 2);
Tensor<double> dxRef({1, 2, 3});
Tensor<double> dscaleRef({2, 3});
Tensor<double> dbiasRef({2, 3});
dxRef.setHostValue(-3.0113333097103734e-06, 0, 0, 0);
dxRef.setHostValue(1.4052918891730595, 0, 0, 1);
dxRef.setHostValue(-1.8737255302307223, 0, 0, 2);
dxRef.setHostValue(1.8737255302307223, 0, 1, 0);
dxRef.setHostValue(-3.7474480491281357, 0, 1, 1);
dxRef.setHostValue(2.342159171288385, 0, 1, 2);
dscaleRef.setHostValue(1.4638475999719223, 0, 0);
dscaleRef.setHostValue(-1.7566171199663065, 0, 1);
dscaleRef.setHostValue(0.8783085599831533, 0, 2);
dscaleRef.setHostValue(1.1710780799775378, 1, 0);
dscaleRef.setHostValue(-4.391542799915767, 1, 1);
dscaleRef.setHostValue(8.783085599831534, 1, 2);
dbiasRef.setHostValue(-1.0, 0, 0);
dbiasRef.setHostValue(2.0, 0, 1);
dbiasRef.setHostValue(-3.0, 0, 2);
dbiasRef.setHostValue(4.0, 1, 0);
dbiasRef.setHostValue(-5.0, 1, 1);
dbiasRef.setHostValue(6.0, 1, 2);
auto tolerance = layernorm::getTolerance<double>();
// Verify each output element: y = (x - mean) * rstd
for(int i = 0; i < 2; i++)
{
for(int j = 0; j < 3; j++)
{
EXPECT_NEAR(dx.getHostValue(0, i, j), dxRef.getHostValue(0, i, j), tolerance);
EXPECT_NEAR(dscale.getHostValue(i, j), dscaleRef.getHostValue(i, j), tolerance);
EXPECT_NEAR(dbias.getHostValue(i, j), dbiasRef.getHostValue(i, j), tolerance);
}
}
}
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropSanityValidationEntire3D)
{
// Input shape: [1, 2, 3] — normalize over last 2 dims (2x3 = 6 features)
// dy = [[[-1, 2, -3], [4, -5, 6]]]
// x = [[[1, 2, 3], [4, 5, 6]]]
//
// mean = (1+2+3+4+5+6)/6 = 3.5
// var = ((1-3.5)^2 + (2-3.5)^2 + (3-3.5)^2 + (4-3.5)^2 + (5-3.5)^2 + (6-3.5)^2) / 6
// = (6.25 + 2.25 + 0.25 + 0.25 + 2.25 + 6.25) / 6 = 17.5 / 6 = 2.9166666666666665
// rstd = 1/sqrt(2.9166666666666665 + 1e-5) = 0.5855390399887689
//
// With scale=1, bias=0 (identity affine):
// a = rstd³ * (sum(dy * scale * x) - sum(dy * scale) * mean) / 6
// b = rstd * sum(dy * scale) / 6 - a * mean
// dx = rstd * dy * scale - a * x - b
// dscale = sum(dy * (x - mean) * rstd)
// dbias = sum(dy)
Tensor<double> dy({1, 2, 3});
Tensor<double> x({1, 2, 3});
Tensor<double> scale({1, 2, 3});
Tensor<double> dx({1, 2, 3});
Tensor<double> dscale({1, 2, 3});
Tensor<double> dbias({1, 2, 3});
dy.setHostValue(-1.0, 0, 0, 0);
dy.setHostValue(2.0, 0, 0, 1);
dy.setHostValue(-3.0, 0, 0, 2);
dy.setHostValue(4.0, 0, 1, 0);
dy.setHostValue(-5.0, 0, 1, 1);
dy.setHostValue(6.0, 0, 1, 2);
x.setHostValue(1.0, 0, 0, 0);
x.setHostValue(2.0, 0, 0, 1);
x.setHostValue(3.0, 0, 0, 2);
x.setHostValue(4.0, 0, 1, 0);
x.setHostValue(5.0, 0, 1, 1);
x.setHostValue(6.0, 0, 1, 2);
scale.fillWithValue(1.0);
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, nullptr, nullptr, 3);
Tensor<double> dxRef({1, 2, 3});
Tensor<double> dscaleRef({1, 2, 3});
Tensor<double> dbiasRef({1, 2, 3});
dxRef.setHostValue(-3.0113333097103734e-06, 0, 0, 0);
dxRef.setHostValue(1.4052918891730595, 0, 0, 1);
dxRef.setHostValue(-1.8737255302307223, 0, 0, 2);
dxRef.setHostValue(1.8737255302307223, 0, 1, 0);
dxRef.setHostValue(-3.7474480491281357, 0, 1, 1);
dxRef.setHostValue(2.342159171288385, 0, 1, 2);
dscaleRef.setHostValue(1.4638475999719223, 0, 0, 0);
dscaleRef.setHostValue(-1.7566171199663065, 0, 0, 1);
dscaleRef.setHostValue(0.8783085599831533, 0, 0, 2);
dscaleRef.setHostValue(1.1710780799775378, 0, 1, 0);
dscaleRef.setHostValue(-4.391542799915767, 0, 1, 1);
dscaleRef.setHostValue(8.783085599831534, 0, 1, 2);
dbiasRef.setHostValue(-1.0, 0, 0, 0);
dbiasRef.setHostValue(2.0, 0, 0, 1);
dbiasRef.setHostValue(-3.0, 0, 0, 2);
dbiasRef.setHostValue(4.0, 0, 1, 0);
dbiasRef.setHostValue(-5.0, 0, 1, 1);
dbiasRef.setHostValue(6.0, 0, 1, 2);
auto tolerance = layernorm::getTolerance<double>();
// Verify each output element: y = (x - mean) * rstd
for(int i = 0; i < 2; i++)
{
for(int j = 0; j < 3; j++)
{
EXPECT_NEAR(dx.getHostValue(0, i, j), dxRef.getHostValue(0, i, j), tolerance);
EXPECT_NEAR(dscale.getHostValue(0, i, j), dscaleRef.getHostValue(0, i, j), tolerance);
EXPECT_NEAR(dbias.getHostValue(0, i, j), dbiasRef.getHostValue(0, i, j), tolerance);
}
}
}
// ============================================================================
// Corner case: all zeros input
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropAllZeros)
{
// All-zero input: mean=0, var=0, rstd=1/sqrt(epsilon)
// a = rstd³ * (sum(dy * scale * x) - sum(dy * scale) * mean) / 4 = 0
// b = rstd * sum(dy * scale) / 4 - a * mean = 0
// dx = rstd * dy * scale - a * x - b = 0
// dscale = sum(dy * (x - mean) * rstd) = 0
// dbias = sum(dy) = 0
Tensor<double> dy({2, 4});
Tensor<double> x({2, 4});
Tensor<double> scale({4});
Tensor<double> mean({2});
Tensor<double> rstd({2});
Tensor<double> dx({2, 4});
Tensor<double> dscale({4});
Tensor<double> dbias({4});
dy.fillWithValue(0.0);
x.fillWithValue(0.0);
mean.fillWithValue(0.0);
rstd.fillWithValue(1.0 / std::sqrt(LAYERNORM_DEFAULT_EPSILON));
for(int i = 0; i < 4; i++)
{
scale.setHostValue(2.0, i);
}
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
for(int f = 0; f < 4; f++)
{
EXPECT_NEAR(dscale.getHostValue(f), 0.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(f), 0.0, tolerance);
for(int b = 0; b < 2; b++)
{
EXPECT_NEAR(dx.getHostValue(b, f), 0.0, tolerance);
}
}
}
// ============================================================================
// Corner case: all ones input
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropAllOnes)
{
// All-one input: mean=1, var=0, rstd=1/sqrt(epsilon)
// a = rstd³ * (sum(dy * scale * x) - sum(dy * scale) * mean) / 5 = 0
// b = rstd * sum(dy * scale) / 5 - a * mean = sum(scale) / std::sqrt(epsilon) / 5 = 1.5 / std::sqrt(epsilon)
// dx = rstd * dy * scale - a * x - b = scale / std::sqrt(epsilon) - sum(scale) / std::sqrt(epsilon) / 5 = 0
// dscale = sum(dy * (x - mean) * rstd) = 0
// dbias = sum(dy) = 3
Tensor<double> dy({3, 5});
Tensor<double> x({3, 5});
Tensor<double> scale({5});
Tensor<double> mean({3});
Tensor<double> rstd({3});
Tensor<double> dx({3, 5});
Tensor<double> dscale({5});
Tensor<double> dbias({5});
dy.fillWithValue(1.0);
x.fillWithValue(1.0);
scale.fillWithValue(1.5);
mean.fillWithValue(1.0);
rstd.fillWithValue(1.0 / std::sqrt(LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
for(int f = 0; f < 5; f++)
{
EXPECT_NEAR(dscale.getHostValue(f), 0.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(f), 3.0, tolerance);
for(int b = 0; b < 3; b++)
{
EXPECT_NEAR(dx.getHostValue(b, f), 0.0, tolerance);
}
}
}
// ============================================================================
// Corner case: constant input (all same non-trivial value)
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropConstantInput)
{
// All elements = 7.0, scale = 1.0: mean=7, var=0
// a = rstd³ * (sum(dy * scale * x) - sum(dy * scale) * mean) / 3 = 0
// b = rstd * sum(dy * scale) / 3 - a * mean = sum(dy) / std::sqrt(epsilon) / 3 = 7.0 / std::sqrt(epsilon)
// dx = rstd * dy * scale - a * x - b = dy / std::sqrt(epsilon) - sum(dy) / std::sqrt(epsilon) / 3 = 0
// dscale = sum(dy * (x - mean) * rstd) = 0
// dbias = sum(dy) = 14.0
Tensor<double> dy({2, 3});
Tensor<double> x({2, 3});
Tensor<double> scale({3});
Tensor<double> mean({2});
Tensor<double> rstd({2});
Tensor<double> dx({2, 3});
Tensor<double> dscale({3});
Tensor<double> dbias({3});
dy.fillWithValue(7.0);
x.fillWithValue(7.0);
scale.fillWithValue(1.0);
mean.fillWithValue(7.0);
rstd.fillWithValue(1.0 / std::sqrt(LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
for(int f = 0; f < 3; f++)
{
EXPECT_NEAR(dscale.getHostValue(f), 0.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(f), 14.0, tolerance);
for(int b = 0; b < 2; b++)
{
EXPECT_NEAR(dx.getHostValue(b, f), 0.0, tolerance);
}
}
}
// ============================================================================
// Corner case: single element per normalized group
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropSingleFeature)
{
// Shape [3, 1]: normalize over dim of size 1
// mean = x, var = 0
// a = rstd³ * (dy * scale * x - dy * scale * mean) = 0
// b = rstd * dy * scale - a * mean = scale / std::sqrt(epsilon) = 2.0 * dy / std::sqrt(epsilon)
// dx = rstd * dy * scale - a * x - b = 2.0 * dy / std::sqrt(epsilon) - 2.0 * dy / std::sqrt(epsilon) = 0
// dscale = sum(dy * (x - mean) * rstd) = 0
// dbias = sum(dy) = -2
Tensor<double> dy({3, 1});
Tensor<double> x({3, 1});
Tensor<double> scale({1});
Tensor<double> mean({3});
Tensor<double> rstd({3});
Tensor<double> dx({3, 1});
Tensor<double> dscale({1});
Tensor<double> dbias({1});
dy.setHostValue(-5.0, 0, 0);
dy.setHostValue(3.0, 1, 0);
dy.setHostValue(0.0, 2, 0);
x.setHostValue(5.0, 0, 0);
x.setHostValue(-3.0, 1, 0);
x.setHostValue(0.0, 2, 0);
scale.setHostValue(2.0, 0);
mean.setHostValue(5.0, 0);
mean.setHostValue(-3.0, 1);
mean.setHostValue(0.0, 2);
rstd.fillWithValue(1.0 / std::sqrt(LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
EXPECT_NEAR(dx.getHostValue(0, 0), 0.0, tolerance);
EXPECT_NEAR(dx.getHostValue(1, 0), 0.0, tolerance);
EXPECT_NEAR(dx.getHostValue(2, 0), 0.0, tolerance);
EXPECT_NEAR(dscale.getHostValue(0), 0.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(0), -2.0, tolerance);
}
// ============================================================================
// Corner case: negative values and mixed signs
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropNegativeAndMixedValues)
{
// Test with negative and mixed-sign values to ensure correct handling
// dy = [-1, -3, 3, 1]
// x = [-3, 1, -1, 3]
// mean = 0
// var = 5
// rstd = 1/sqrt(5 + 1e-5)
// a = rstd³ * (sum(dy * scale * x) - sum(dy * scale) * mean) / 4 = 0
// b = rstd * sum(dy * scale) / 4 - a * mean = sum(scale) / std::sqrt(epsilon) / 4 = 0
// dx = rstd * dy * scale - a * x - b = dy / std::sqrt(5 + epsilon) = [-1, -3, 3, 1] / std::sqrt(5 + epsilon)
// dscale = dy * (x - mean) * rstd = [3, -3, -3, 3] / std::sqrt(5 + epsilon)
// dbias = dy = [-1, -3, 3, 1]
Tensor<double> dy({1, 4});
Tensor<double> x({1, 4});
Tensor<double> scale({4});
Tensor<double> mean({1});
Tensor<double> rstd({1});
Tensor<double> dx({1, 4});
Tensor<double> dscale({4});
Tensor<double> dbias({4});
dy.setHostValue(-1.0, 0, 0);
dy.setHostValue(-3.0, 0, 1);
dy.setHostValue(3.0, 0, 2);
dy.setHostValue(1.0, 0, 3);
x.setHostValue(-3.0, 0, 0);
x.setHostValue(1.0, 0, 1);
x.setHostValue(-1.0, 0, 2);
x.setHostValue(3.0, 0, 3);
scale.fillWithValue(1.0);
mean.fillWithValue(0.0);
rstd.fillWithValue(1.0 / std::sqrt(5.0 + LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
// dx and dbias should be antisymmetric: dx[0]=-dx[3], dx[1]=-dx[2]
EXPECT_NEAR(dx.getHostValue(0, 0), -dx.getHostValue(0, 3), tolerance);
EXPECT_NEAR(dx.getHostValue(0, 1), -dx.getHostValue(0, 2), tolerance);
EXPECT_NEAR(dbias.getHostValue(0), -dbias.getHostValue(3), tolerance);
EXPECT_NEAR(dbias.getHostValue(1), -dbias.getHostValue(2), tolerance);
// dscale should be symmetric: dscale[0]=dscale[3], dscale[1]=dscale[2]
EXPECT_NEAR(dscale.getHostValue(0), dscale.getHostValue(3), tolerance);
EXPECT_NEAR(dscale.getHostValue(1), dscale.getHostValue(2), tolerance);
// And the actual values
EXPECT_NEAR(dx.getHostValue(0, 0), -1.0 * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dx.getHostValue(0, 1), -3.0 * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dx.getHostValue(0, 2), 3.0 * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dx.getHostValue(0, 3), 1.0 * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dscale.getHostValue(0), 3.0 * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dscale.getHostValue(1), -3.0 * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dscale.getHostValue(2), -3.0 * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dscale.getHostValue(3), 3.0 * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dbias.getHostValue(0), -1.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(1), -3.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(2), 3.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(3), 1.0, tolerance);
}
// ============================================================================
// Numerical stability: large values with small variance
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropLargeValueNumericalStability)
{
// Values clustered around 1e15 with small relative differences.
// Naive one-pass (E[x²] - E[x]²) would suffer catastrophic cancellation here
// because E[x²] ≈ 1e30 and E[x]² ≈ 1e30, so their difference loses all precision.
// Welford's algorithm computes variance from centered deltas, avoiding this.
//
// dy = [-1e15 - 1, -1e15 - 2, -1e15 - 3, -1e15 - 4]
// x = [1e15 + 1, 1e15 + 2, 1e15 + 3, 1e15 + 4]
// mean = 1e15 + 2.5
// var = 1.25 (same as small-value case, independent of offset)
// rstd = 1/sqrt(1.25 + 1e-5)
// a = rstd³ * (sum(dy * scale * x) - sum(dy * scale) * mean) / 4 = 0
// b = rstd * sum(dy * scale) / 4 - a * mean = (-1e15 - 2.5) / std::sqrt(1.25 + 1e-5) = -894423613312620.2
// dx = rstd * dy * scale - a * x - b = [1.375, 0.5, -0.375, -1.375]
// dscale = dy * (x - mean) * rstd = [1.5e+15 + 1.5, 5.0e+14 + 1.0, -5.0e+14 - 1.5, -1.5e+15 - 6.0] / std::sqrt(1.25 + 1e-5) = [1.34163542e+15, 4.47211807e+14, -4.47211807e+14, -1.34163542e+15]
// dbias = dy = [-1e15 - 1, -1e15 - 2, -1e15 - 3, -1e15 - 4]
Tensor<double> dy({1, 4});
Tensor<double> x({1, 4});
Tensor<double> scale({4});
Tensor<double> mean({1});
Tensor<double> rstd({1});
Tensor<double> dx({1, 4});
Tensor<double> dscale({4});
Tensor<double> dbias({4});
const double offset = 1.0e15;
dy.setHostValue(-offset - 1.0, 0, 0);
dy.setHostValue(-offset - 2.0, 0, 1);
dy.setHostValue(-offset - 3.0, 0, 2);
dy.setHostValue(-offset - 4.0, 0, 3);
x.setHostValue(offset + 1.0, 0, 0);
x.setHostValue(offset + 2.0, 0, 1);
x.setHostValue(offset + 3.0, 0, 2);
x.setHostValue(offset + 4.0, 0, 3);
scale.fillWithValue(1.0);
mean.fillWithValue(offset + 2.5);
rstd.fillWithValue(1.0 / std::sqrt(1.25 + LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
// Normalized output should match the small-value case exactly
EXPECT_NEAR(dx.getHostValue(0, 0), 1.375, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 1), 0.5, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 2), -0.375, tolerance);
EXPECT_NEAR(dx.getHostValue(0, 3), -1.375, tolerance);
EXPECT_NEAR(dscale.getHostValue(0), (1.5e15 + 1.5) * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dscale.getHostValue(1), (0.5e15 + 1.0) * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dscale.getHostValue(2), (-0.5e15 - 1.5) * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dscale.getHostValue(3), (-1.5e15 - 6.0) * rstd.getHostValue(0), tolerance);
EXPECT_NEAR(dbias.getHostValue(0), -1.0e15 - 1.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(1), -1.0e15 - 2.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(2), -1.0e15 - 3.0, tolerance);
EXPECT_NEAR(dbias.getHostValue(3), -1.0e15 - 4.0, tolerance);
}
// ============================================================================
// Realistic shape: typical transformer hidden dim
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp32, BpropTypicalTransformerShape)
{
// Batch=2, SeqLen=8, Hidden=64 — normalize over last dim (hidden)
Tensor<float> dy({2, 8, 64});
Tensor<float> x({2, 8, 64});
Tensor<float> scale({64});
Tensor<float> mean({2, 8});
Tensor<float> rstd({2, 8});
Tensor<float> dx({2, 8, 64});
Tensor<float> dscale({64});
Tensor<float> dbias({64});
dy.fillWithRandomValues(-1.0f, 1.0f, 42);
x.fillWithRandomValues(-1.0f, 1.0f, 43);
scale.fillWithValue(1.0f);
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 8; ++j)
{
float batchMean = 0.0f;
for(size_t k = 0; k < 64; ++k)
{
batchMean += x.getHostValue(i, j, k);
}
batchMean /= 64.0f;
mean.setHostValue(batchMean, i, j);
float batchVariance = 0.0f;
for(size_t k = 0; k < 64; ++k)
{
const float diff = x.getHostValue(i, j, k) - batchMean;
batchVariance += diff * diff;
}
batchVariance /= 64.0f;
rstd.setHostValue(
1.0f / std::sqrt(batchVariance + static_cast<float>(LAYERNORM_DEFAULT_EPSILON)),
i,
j);
}
}
CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<float>();
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 8; ++j)
{
const float batchRstd = rstd.getHostValue(i, j);
float sumDyScaleX = 0.0f;
float sumDyScale = 0.0f;
for(size_t k = 0; k < 64; ++k)
{
sumDyScaleX
+= dy.getHostValue(i, j, k) * scale.getHostValue(k) * x.getHostValue(i, j, k);
sumDyScale += dy.getHostValue(i, j, k) * scale.getHostValue(k);
}
const float a = batchRstd * batchRstd * batchRstd
* (sumDyScaleX - sumDyScale * mean.getHostValue(i, j)) / 64.0f;
const float b = batchRstd * sumDyScale / 64.0f - a * mean.getHostValue(i, j);
for(size_t k = 0; k < 64; ++k)
{
const float outDx = batchRstd * dy.getHostValue(i, j, k) * scale.getHostValue(k)
- a * x.getHostValue(i, j, k) - b;
EXPECT_NEAR(outDx, dx.getHostValue(i, j, k), tolerance);
}
}
}
for(size_t k = 0; k < 64; ++k)
{
float batchDscale = 0.0f;
float batchDbias = 0.0f;
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 8; ++j)
{
batchDscale += dy.getHostValue(i, j, k)
* (x.getHostValue(i, j, k) - mean.getHostValue(i, j))
* rstd.getHostValue(i, j);
batchDbias += dy.getHostValue(i, j, k);
}
}
EXPECT_NEAR(batchDscale, dscale.getHostValue(k), tolerance);
EXPECT_NEAR(batchDbias, dbias.getHostValue(k), tolerance);
}
}
// ============================================================================
// Type compatibility: various type combinations
// ============================================================================
template <typename T1, typename T2>
struct TypePair
{
using First = T1;
using Second = T2;
};
using LayernormBpropTypes = ::testing::Types<TypePair<float, float>,
TypePair<half, float>,
TypePair<bfloat16, float>,
TypePair<double, double>>;
template <class T>
class CpuFpReferenceLayernormBpropTyped : public ::testing::Test
{
};
TYPED_TEST_SUITE(CpuFpReferenceLayernormBpropTyped, LayernormBpropTypes, );
TYPED_TEST(CpuFpReferenceLayernormBpropTyped, BpropRunsWithoutError)
{
using DyType = typename TypeParam::First;
using ScaleType = typename TypeParam::Second;
Tensor<DyType> dy({2, 8, 32});
Tensor<DyType> x({2, 8, 32});
Tensor<ScaleType> scale({32});
Tensor<DyType> mean({2, 8});
Tensor<DyType> rstd({2, 8});
Tensor<DyType> dx({2, 8, 32});
Tensor<ScaleType> dscale({32});
Tensor<ScaleType> dbias({32});
dy.fillWithValue(safeTestTypeCast<DyType>(0.0));
x.fillWithValue(safeTestTypeCast<DyType>(1.0));
scale.fillWithValue(safeTestTypeCast<ScaleType>(1.0));
mean.fillWithValue(safeTestTypeCast<DyType>(1.0));
rstd.fillWithValue(safeTestTypeCast<DyType>(1.0 / std::sqrt(LAYERNORM_DEFAULT_EPSILON)));
// Should not throw
CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
// All zeroes dy -> all dx, dscale and dbias are zero
for(size_t k = 0; k < 32; ++k)
{
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 8; ++j)
{
EXPECT_NEAR(static_cast<float>(dx.getHostValue(i, j, k)), 0.0f, tolerance);
}
}
EXPECT_NEAR(static_cast<float>(dscale.getHostValue(k)), 0.0f, tolerance);
EXPECT_NEAR(static_cast<float>(dbias.getHostValue(k)), 0.0f, tolerance);
}
}
// ============================================================================
// Multi-dim normalization: normalize over last 2 dims
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropNormalizeLastTwoDims)
{
// Shape [2, 3, 4], normalize over last 2 dims (3x4 = 12 features)
// Each of the 2 batches is independently normalized over 12 elements
Tensor<double> dy({2, 3, 4});
Tensor<double> x({2, 3, 4});
Tensor<double> scale({3, 4});
Tensor<double> mean({2});
Tensor<double> rstd({2});
Tensor<double> dx({2, 3, 4});
Tensor<double> dscale({3, 4});
Tensor<double> dbias({3, 4});
// Batch 0: dy values 0.1..1.2, x values 1..12, mean = (1+2+...+12)/12 = 78/12 = 6.5
double batchVar = 0.0;
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 4; j++)
{
dy.setHostValue(static_cast<double>(i * 4 + j + 1) / 10.0, 0, i, j);
auto xVal = static_cast<double>(i * 4 + j + 1);
x.setHostValue(xVal, 0, i, j);
batchVar += (xVal - 6.5) * (xVal - 6.5);
}
}
mean.setHostValue(6.5, 0);
rstd.setHostValue(1.0 / std::sqrt(batchVar + LAYERNORM_DEFAULT_EPSILON), 0);
// Batch 1: dy values 1.3..2.4, x values 13..24, mean = (13+14+...+24)/12 = 222/12 = 18.5
batchVar = 0.0;
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 4; j++)
{
dy.setHostValue(static_cast<double>(i * 4 + j + 13) / 10.0, 1, i, j);
auto xVal = static_cast<double>(i * 4 + j + 13);
x.setHostValue(xVal, 1, i, j);
batchVar += (xVal - 18.5) * (xVal - 18.5);
}
}
mean.setHostValue(18.5, 1);
rstd.setHostValue(1.0 / std::sqrt(batchVar + LAYERNORM_DEFAULT_EPSILON), 1);
scale.fillWithValue(1.0);
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 2);
auto tolerance = layernorm::getTolerance<double>();
for(size_t i = 0; i < 2; ++i)
{
const double batchMean = mean.getHostValue(i);
const double batchRstd = rstd.getHostValue(i);
double sumDyScaleX = 0.0;
double sumDyScale = 0.0;
for(size_t j = 0; j < 3; ++j)
{
for(size_t k = 0; k < 4; ++k)
{
sumDyScaleX += dy.getHostValue(i, j, k) * scale.getHostValue(j, k)
* x.getHostValue(i, j, k);
sumDyScale += dy.getHostValue(i, j, k) * scale.getHostValue(j, k);
}
}
const double a
= batchRstd * batchRstd * batchRstd * (sumDyScaleX - sumDyScale * batchMean) / 12.0;
const double b = batchRstd * sumDyScale / 12.0 - a * batchMean;
for(size_t j = 0; j < 3; ++j)
{
for(size_t k = 0; k < 4; ++k)
{
const double outDx = batchRstd * dy.getHostValue(i, j, k) * scale.getHostValue(j, k)
- a * x.getHostValue(i, j, k) - b;
EXPECT_NEAR(outDx, dx.getHostValue(i, j, k), tolerance);
}
}
}
for(size_t k = 0; k < 4; ++k)
{
for(size_t j = 0; j < 3; ++j)
{
double outDscale = 0.0;
double outDbias = 0.0;
for(size_t i = 0; i < 2; ++i)
{
outDscale += dy.getHostValue(i, j, k)
* (x.getHostValue(i, j, k) - mean.getHostValue(i))
* rstd.getHostValue(i);
outDbias += dy.getHostValue(i, j, k);
}
EXPECT_NEAR(outDscale, dscale.getHostValue(j, k), tolerance);
EXPECT_NEAR(outDbias, dbias.getHostValue(j, k), tolerance);
}
}
}
// ============================================================================
// Corner case: "diagonal-like" pattern — one hot-per-row
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropOneHotRows)
{
// Shape [3, 3], each row is a one-hot vector
// Row 0: [1, 0, 0], mean=1/3, var=(4/9+1/9+1/9)/3=2/9
// Row 1: [0, 1, 0], same statistics by symmetry
// Row 2: [0, 0, 1], same statistics by symmetry
//
// All rows should produce the same set of output values (permuted)
Tensor<double> dy({3, 3});
Tensor<double> x({3, 3});
Tensor<double> scale({3});
Tensor<double> mean({3});
Tensor<double> rstd({3});
Tensor<double> dx({3, 3});
Tensor<double> dscale({3});
Tensor<double> dbias({3});
dy.fillWithValue(0.0);
dy.setHostValue(0.1, 0, 0);
dy.setHostValue(0.1, 1, 1);
dy.setHostValue(0.1, 2, 2);
x.fillWithValue(0.0);
x.setHostValue(1.0, 0, 0);
x.setHostValue(1.0, 1, 1);
x.setHostValue(1.0, 2, 2);
scale.fillWithValue(1.0);
mean.fillWithValue(1.0 / 3.0);
rstd.fillWithValue(1.0 / std::sqrt(2.0 / 9.0 + LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
// The "hot" position output should be identical across rows
// dx[0,0] should equal dx[1,1] and dx[2,2]
EXPECT_NEAR(dx.getHostValue(0, 0), dx.getHostValue(1, 1), tolerance);
EXPECT_NEAR(dx.getHostValue(1, 1), dx.getHostValue(2, 2), tolerance);
// The "cold" position output should be identical across rows
EXPECT_NEAR(dx.getHostValue(0, 1), dx.getHostValue(0, 2), tolerance);
EXPECT_NEAR(dx.getHostValue(0, 1), dx.getHostValue(1, 0), tolerance);
}
// ============================================================================
// Verify per-feature scale and bias
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropPerFeatureScale)
{
// Verify that different scale per feature is applied correctly
// dy = [[0.1, 0.2, 0.3, 0.4]]
// x = [[1, 2, 3, 4]]
// scale = [1, 2, 3, 4]
Tensor<double> dy({1, 4});
Tensor<double> x({1, 4});
Tensor<double> scale({4});
Tensor<double> mean({1});
Tensor<double> rstd({1});
Tensor<double> dx({1, 4});
Tensor<double> dscale({4});
Tensor<double> dbias({4});
dy.setHostValue(0.1, 0, 0);
dy.setHostValue(0.2, 0, 1);
dy.setHostValue(0.3, 0, 2);
dy.setHostValue(0.4, 0, 3);
x.setHostValue(1.0, 0, 0);
x.setHostValue(2.0, 0, 1);
x.setHostValue(3.0, 0, 2);
x.setHostValue(4.0, 0, 3);
scale.setHostValue(1.0, 0);
scale.setHostValue(2.0, 1);
scale.setHostValue(3.0, 2);
scale.setHostValue(4.0, 3);
// mean=2.5, var=1.25, rstd=1/sqrt(1.25+1e-5)
mean.setHostValue(2.5, 0);
rstd.setHostValue(1.0 / std::sqrt(1.25 + LAYERNORM_DEFAULT_EPSILON), 0);
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
// a = rstd³ * (sum(dy[i] * scale[i] * x[i]) - sum(dy[i] * scale[i]) * mean)
// b = rstd * sum(dy[i] * scale[i]) - a * mean
// dx[i] = rstd * dy[i] * scale[i] - a * x[i] - b
// dscale[i] = dy[i] * (x[i] - mean) * rstd
// dbias[i] = dy[i]
const double batchRstd = rstd.getHostValue(0);
double sumDyScaleX = 0.0;
double sumDyScale = 0.0;
for(size_t k = 0; k < 4; ++k)
{
sumDyScaleX += dy.getHostValue(0, k) * scale.getHostValue(k) * x.getHostValue(0, k);
sumDyScale += dy.getHostValue(0, k) * scale.getHostValue(k);
}
const double a = batchRstd * batchRstd * batchRstd
* (sumDyScaleX - sumDyScale * mean.getHostValue(0)) / 4.0;
const double b = batchRstd * sumDyScale / 4.0 - a * mean.getHostValue(0);
for(size_t k = 0; k < 4; ++k)
{
const double outDx = batchRstd * dy.getHostValue(0, k) * scale.getHostValue(k)
- a * x.getHostValue(0, k) - b;
EXPECT_NEAR(outDx, dx.getHostValue(0, k), tolerance);
}
for(size_t k = 0; k < 4; ++k)
{
const double outDscale = dy.getHostValue(0, k)
* (x.getHostValue(0, k) - mean.getHostValue(0))
* rstd.getHostValue(0);
const double outDbias = dy.getHostValue(0, k);
EXPECT_NEAR(outDscale, dscale.getHostValue(k), tolerance);
EXPECT_NEAR(outDbias, dbias.getHostValue(k), tolerance);
}
}
// ============================================================================
// Error handling: invalid dimensions
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp32, BpropThrowsOnInvalidDimensions)
{
const Tensor<float> dy({2, 4});
const Tensor<float> x({2, 4});
const Tensor<float> scale({4});
const Tensor<float> mean({2});
const Tensor<float> rstd({2});
Tensor<float> dx({2, 4});
Tensor<float> dscale({4});
Tensor<float> dbias({4});
Tensor<float> invalidDscale({2});
Tensor<float> invalidDbias({2});
const Tensor<float> invalidMean({4});
// scale and dscale must have the same dimensions
EXPECT_THROW(
CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, invalidDscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1),
std::runtime_error);
// bias and dbias must have the same dimensions
EXPECT_THROW(
CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, dscale, invalidDbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1),
std::runtime_error);
// mean and rstd must have the same dimensions
EXPECT_THROW(CpuFpReferenceLayernorm::bprop(dy,
x,
scale,
dx,
dscale,
invalidDbias,
LAYERNORM_DEFAULT_EPSILON,
&invalidMean,
&rstd,
1),
std::runtime_error);
}
// ============================================================================
// Error handling: invalid normalized dimension count
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp32, BpropThrowsOnInvalidNormalizedDimCount)
{
const Tensor<float> dy({2, 4});
const Tensor<float> x({2, 4});
const Tensor<float> scale({4});
const Tensor<float> mean({2});
const Tensor<float> rstd({2});
Tensor<float> dx({2, 4});
Tensor<float> dscale({4});
Tensor<float> dbias({4});
// normalizedDimCount = 0 is invalid
EXPECT_THROW(CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 0),
std::runtime_error);
// normalizedDimCount > ndim is invalid
EXPECT_THROW(CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 3),
std::runtime_error);
}
// ============================================================================
// Error handling: invalid mean or rstd nullptr
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp32, BpropThrowsOnOnlyOneNullptrMeanOrRstd)
{
const Tensor<float> dy({2, 4});
const Tensor<float> x({2, 4});
const Tensor<float> scale({4});
const Tensor<float> mean({2});
const Tensor<float> rstd({2});
Tensor<float> dx({2, 4});
Tensor<float> dscale({4});
Tensor<float> dbias({4});
const hipdnn_data_sdk::utilities::TensorBase<float>* nullTensor = nullptr;
// mean and rstd must both be specified or both be nullptr
EXPECT_THROW(
CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, nullTensor, &rstd, 1),
std::runtime_error);
EXPECT_THROW(
CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, nullTensor, 1),
std::runtime_error);
}
// ============================================================================
// Error handling: scalar (0D) tensor
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp32, BpropThrowsOnScalarTensor)
{
const Tensor<float> dy({});
const Tensor<float> x({});
const Tensor<float> scale({});
const Tensor<float> mean({});
const Tensor<float> rstd({});
Tensor<float> dx({});
Tensor<float> dscale({});
Tensor<float> dbias({});
EXPECT_THROW(CpuFpReferenceLayernorm::bprop(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1),
std::runtime_error);
}
// ============================================================================
// Dimensionality coverage: 1D tensor
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, Bprop1D)
{
// Shape [5] — normalize entire 1D tensor (single group, no batch dim)
// dy = [-1, -2, -3, -4, -5]
// x = [2, 4, 6, 8, 10]
// mean = 6, var = (16+4+0+4+16)/5 = 8
// rstd = 1/sqrt(8 + 1e-5)
Tensor<double> dy({5});
Tensor<double> x({5});
Tensor<double> scale({5});
Tensor<double> mean({1});
Tensor<double> rstd({1});
Tensor<double> dx({5});
Tensor<double> dscale({5});
Tensor<double> dbias({5});
dy.setHostValue(-1, 0);
dy.setHostValue(-2, 1);
dy.setHostValue(-3, 2);
dy.setHostValue(-4, 3);
dy.setHostValue(-5, 4);
x.setHostValue(2.0, 0);
x.setHostValue(4.0, 1);
x.setHostValue(6.0, 2);
x.setHostValue(8.0, 3);
x.setHostValue(10.0, 4);
scale.fillWithValue(1.0);
mean.fillWithValue(6.0);
rstd.fillWithValue(1.0 / std::sqrt(8.0 + LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
const double batchRstd = rstd.getHostValue(0);
double sumDyScaleX = 0.0;
double sumDyScale = 0.0;
for(size_t k = 0; k < 5; ++k)
{
sumDyScaleX += dy.getHostValue(k) * scale.getHostValue(k) * x.getHostValue(k);
sumDyScale += dy.getHostValue(k) * scale.getHostValue(k);
}
const double a = batchRstd * batchRstd * batchRstd
* (sumDyScaleX - sumDyScale * mean.getHostValue(0)) / 5.0;
const double b = batchRstd * sumDyScale / 5.0 - a * mean.getHostValue(0);
for(size_t k = 0; k < 5; ++k)
{
const double outDx
= batchRstd * dy.getHostValue(k) * scale.getHostValue(k) - a * x.getHostValue(k) - b;
EXPECT_NEAR(outDx, dx.getHostValue(k), tolerance);
}
for(size_t k = 0; k < 5; ++k)
{
const double outDscale = dy.getHostValue(k) * (x.getHostValue(k) - mean.getHostValue(0))
* rstd.getHostValue(0);
const double outDbias = dy.getHostValue(k);
EXPECT_NEAR(outDscale, dscale.getHostValue(k), tolerance);
EXPECT_NEAR(outDbias, dbias.getHostValue(k), tolerance);
}
}
// ============================================================================
// Dimensionality coverage: 4D tensor
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, Bprop4DNormalizeLast1)
{
// Shape [2, 3, 2, 4], normalizedDimCount=1: normalize over last dim (4)
// Batch dims = [2, 3, 2], 12 independent groups of 4 elements each
Tensor<double> dy({2, 3, 2, 4});
Tensor<double> x({2, 3, 2, 4});
Tensor<double> scale({4});
Tensor<double> mean({2, 3, 2});
Tensor<double> rstd({2, 3, 2});
Tensor<double> dx({2, 3, 2, 4});
Tensor<double> dscale({4});
Tensor<double> dbias({4});
// Fill with sequential values so each group has a known pattern
double val = 1.0;
for(int b = 0; b < 2; b++)
{
for(int c = 0; c < 3; c++)
{
for(int h = 0; h < 2; h++)
{
for(int w = 0; w < 4; w++)
{
dy.setHostValue(-val / 10.0, b, c, h, w);
x.setHostValue(val, b, c, h, w);
val += 1.0;
}
}
}
}
scale.fillWithValue(1.0);
mean.fillWithValue(2.5);
rstd.fillWithValue(1.0 / std::sqrt(1.25 + LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 1);
auto tolerance = layernorm::getTolerance<double>();
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 3; ++j)
{
for(size_t k = 0; k < 2; ++k)
{
const double batchRstd = rstd.getHostValue(i, j, k);
double sumDyScaleX = 0.0;
double sumDyScale = 0.0;
for(size_t l = 0; l < 4; ++l)
{
sumDyScaleX += dy.getHostValue(i, j, k, l) * scale.getHostValue(l)
* x.getHostValue(i, j, k, l);
sumDyScale += dy.getHostValue(i, j, k, l) * scale.getHostValue(l);
}
const double a = batchRstd * batchRstd * batchRstd
* (sumDyScaleX - sumDyScale * mean.getHostValue(i, j, k)) / 4.0;
const double b = batchRstd * sumDyScale / 4.0 - a * mean.getHostValue(i, j, k);
for(size_t l = 0; l < 4; ++l)
{
const double outDx
= batchRstd * dy.getHostValue(i, j, k, l) * scale.getHostValue(l)
- a * x.getHostValue(i, j, k, l) - b;
EXPECT_NEAR(outDx, dx.getHostValue(i, j, k, l), tolerance);
}
}
}
}
for(size_t l = 0; l < 4; ++l)
{
double outDscale = 0.0;
double outDbias = 0.0;
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 3; ++j)
{
for(size_t k = 0; k < 2; ++k)
{
outDscale += dy.getHostValue(i, j, k, l)
* (x.getHostValue(i, j, k, l) - mean.getHostValue(i, j, k))
* rstd.getHostValue(i, j, k);
outDbias += dy.getHostValue(i, j, k, l);
}
}
}
EXPECT_NEAR(outDscale, dscale.getHostValue(l), tolerance);
EXPECT_NEAR(outDbias, dbias.getHostValue(l), tolerance);
}
}
TEST(TestCpuFpReferenceLayernormBackwardFp64, Bprop4DNormalizeLast3)
{
// Shape [2, 3, 2, 4], normalizedDimCount=3: normalize over last 3 dims (3*2*4=24)
// Batch dims = [2], 2 independent groups of 24 elements each
Tensor<double> dy({2, 3, 2, 4});
Tensor<double> x({2, 3, 2, 4});
Tensor<double> scale({3, 2, 4});
Tensor<double> mean({2});
Tensor<double> rstd({2});
Tensor<double> dx({2, 3, 2, 4});
Tensor<double> dscale({3, 2, 4});
Tensor<double> dbias({3, 2, 4});
// Batch 0: values 1..24, Batch 1: values 25..48
double val = 1.0;
for(int b = 0; b < 2; b++)
{
for(int c = 0; c < 3; c++)
{
for(int h = 0; h < 2; h++)
{
for(int w = 0; w < 4; w++)
{
dy.setHostValue(-val / 10.0, b, c, h, w);
x.setHostValue(val, b, c, h, w);
val += 1.0;
}
}
}
}
scale.fillWithValue(1.0);
mean.fillWithValue(12.5);
rstd.fillWithValue(1.0 / std::sqrt(47.91666666667 + LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 3);
auto tolerance = layernorm::getTolerance<double>();
for(size_t i = 0; i < 2; ++i)
{
const double batchRstd = rstd.getHostValue(i);
double sumDyScaleX = 0.0;
double sumDyScale = 0.0;
for(size_t j = 0; j < 3; ++j)
{
for(size_t k = 0; k < 2; ++k)
{
for(size_t l = 0; l < 4; ++l)
{
sumDyScaleX += dy.getHostValue(i, j, k, l) * scale.getHostValue(j, k, l)
* x.getHostValue(i, j, k, l);
sumDyScale += dy.getHostValue(i, j, k, l) * scale.getHostValue(j, k, l);
}
}
}
const double a = batchRstd * batchRstd * batchRstd
* (sumDyScaleX - sumDyScale * mean.getHostValue(i)) / 24.0;
const double b = batchRstd * sumDyScale / 24.0 - a * mean.getHostValue(i);
for(size_t j = 0; j < 3; ++j)
{
for(size_t k = 0; k < 2; ++k)
{
for(size_t l = 0; l < 4; ++l)
{
const double outDx
= batchRstd * dy.getHostValue(i, j, k, l) * scale.getHostValue(j, k, l)
- a * x.getHostValue(i, j, k, l) - b;
EXPECT_NEAR(outDx, dx.getHostValue(i, j, k, l), tolerance);
}
}
}
}
for(size_t j = 0; j < 3; ++j)
{
for(size_t k = 0; k < 2; ++k)
{
for(size_t l = 0; l < 4; ++l)
{
double outDscale = 0.0;
double outDbias = 0.0;
for(size_t i = 0; i < 2; ++i)
{
outDscale += dy.getHostValue(i, j, k, l)
* (x.getHostValue(i, j, k, l) - mean.getHostValue(i))
* rstd.getHostValue(i);
outDbias += dy.getHostValue(i, j, k, l);
}
EXPECT_NEAR(outDscale, dscale.getHostValue(j, k, l), tolerance);
EXPECT_NEAR(outDbias, dbias.getHostValue(j, k, l), tolerance);
}
}
}
}
// ============================================================================
// Full-tensor normalization (normalizedDimCount == ndim)
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, BpropFullTensorNormalization)
{
// Shape [2, 3], normalizedDimCount=2 means normalize entire tensor per "batch"
// But there are no batch dims here, so entire tensor is one group
// x = [[-1, 2, -3], [4, -5, 6]]
// x = [[1, 2, 3], [4, 5, 6]]
// mean = 3.5, var = 17.5/6 = 2.9166...
Tensor<double> dy({2, 3});
Tensor<double> x({2, 3});
Tensor<double> scale({2, 3});
Tensor<double> mean({1});
Tensor<double> rstd({1});
Tensor<double> dx({2, 3});
Tensor<double> dscale({2, 3});
Tensor<double> dbias({2, 3});
dy.setHostValue(-1.0, 0, 0);
dy.setHostValue(2.0, 0, 1);
dy.setHostValue(-3.0, 0, 2);
dy.setHostValue(4.0, 1, 0);
dy.setHostValue(-5.0, 1, 1);
dy.setHostValue(6.0, 1, 2);
x.setHostValue(1.0, 0, 0);
x.setHostValue(2.0, 0, 1);
x.setHostValue(3.0, 0, 2);
x.setHostValue(4.0, 1, 0);
x.setHostValue(5.0, 1, 1);
x.setHostValue(6.0, 1, 2);
scale.fillWithValue(1.0);
mean.fillWithValue(3.5);
rstd.fillWithValue(1.0 / std::sqrt(2.916666667 + LAYERNORM_DEFAULT_EPSILON));
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 2);
auto tolerance = layernorm::getTolerance<double>();
const double batchRstd = rstd.getHostValue(0);
double sumDyScaleX = 0.0;
double sumDyScale = 0.0;
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 3; ++j)
{
sumDyScaleX += dy.getHostValue(i, j) * scale.getHostValue(i, j) * x.getHostValue(i, j);
sumDyScale += dy.getHostValue(i, j) * scale.getHostValue(i, j);
}
}
const double a = batchRstd * batchRstd * batchRstd
* (sumDyScaleX - sumDyScale * mean.getHostValue(0)) / 6.0;
const double b = batchRstd * sumDyScale / 6.0 - a * mean.getHostValue(0);
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 3; ++j)
{
const double outDx = batchRstd * dy.getHostValue(i, j) * scale.getHostValue(i, j)
- a * x.getHostValue(i, j) - b;
EXPECT_NEAR(outDx, dx.getHostValue(i, j), tolerance);
}
}
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 3; ++j)
{
double outDscale = 0.0;
double outDbias = 0.0;
outDscale += dy.getHostValue(i, j) * (x.getHostValue(i, j) - mean.getHostValue(0))
* rstd.getHostValue(0);
outDbias += dy.getHostValue(i, j);
EXPECT_NEAR(outDscale, dscale.getHostValue(i, j), tolerance);
EXPECT_NEAR(outDbias, dbias.getHostValue(i, j), tolerance);
}
}
}
// ============================================================================
// Dimensionality coverage: 5D tensor
// ============================================================================
TEST(TestCpuFpReferenceLayernormBackwardFp64, Bprop5DNormalizeLast2)
{
// Shape [2, 2, 3, 4, 5], normalizedDimCount=2: normalize over last 2 dims (4*5=20)
// Batch dims = [2, 2, 3], 12 independent groups of 20 elements each
Tensor<double> dy({2, 2, 3, 4, 5});
Tensor<double> x({2, 2, 3, 4, 5});
Tensor<double> scale({4, 5});
Tensor<double> mean({2, 2, 3});
Tensor<double> rstd({2, 2, 3});
Tensor<double> dx({2, 2, 3, 4, 5});
Tensor<double> dscale({4, 5});
Tensor<double> dbias({4, 5});
dy.fillWithRandomValues(-5.0, 5.0, 123);
x.fillWithRandomValues(-5.0, 5.0, 123);
scale.fillWithValue(1.0);
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 2; ++j)
{
for(size_t k = 0; k < 3; ++k)
{
double batchMean = 0.0;
for(size_t l = 0; l < 4; ++l)
{
for(size_t m = 0; m < 5; ++m)
{
batchMean += x.getHostValue(i, j, k, l, m);
}
}
batchMean /= 12.0;
mean.setHostValue(batchMean, i, j, k);
double batchVar = 0.0;
for(size_t l = 0; l < 4; ++l)
{
for(size_t m = 0; m < 5; ++m)
{
const double diff = x.getHostValue(i, j, k, l, m) - batchMean;
batchVar += diff * diff;
}
}
batchVar /= 12.0;
rstd.setHostValue(1.0 / std::sqrt(batchVar + LAYERNORM_DEFAULT_EPSILON), i, j, k);
}
}
}
CpuFpReferenceLayernorm::bprop<double, double, double, double, double>(
dy, x, scale, dx, dscale, dbias, LAYERNORM_DEFAULT_EPSILON, &mean, &rstd, 2);
auto tolerance = layernorm::getTolerance<double>();
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 2; ++j)
{
for(size_t k = 0; k < 3; ++k)
{
const double batchRstd = rstd.getHostValue(i, j, k);
double sumDyScaleX = 0.0;
double sumDyScale = 0.0;
for(size_t l = 0; l < 4; ++l)
{
for(size_t m = 0; m < 5; ++m)
{
sumDyScaleX += dy.getHostValue(i, j, k, l, m) * scale.getHostValue(l, m)
* x.getHostValue(i, j, k, l, m);
sumDyScale += dy.getHostValue(i, j, k, l, m) * scale.getHostValue(l, m);
}
}
const double a = batchRstd * batchRstd * batchRstd
* (sumDyScaleX - sumDyScale * mean.getHostValue(i, j, k)) / 20.0;
const double b = batchRstd * sumDyScale / 20.0 - a * mean.getHostValue(i, j, k);
for(size_t l = 0; l < 4; ++l)
{
for(size_t m = 0; m < 5; ++m)
{
const double outDx
= batchRstd * dy.getHostValue(i, j, k, l, m) * scale.getHostValue(l, m)
- a * x.getHostValue(i, j, k, l, m) - b;
EXPECT_NEAR(outDx, dx.getHostValue(i, j, k, l, m), tolerance);
}
}
}
}
}
for(size_t l = 0; l < 4; ++l)
{
for(size_t m = 0; m < 5; ++m)
{
double outDscale = 0.0;
double outDbias = 0.0;
for(size_t i = 0; i < 2; ++i)
{
for(size_t j = 0; j < 2; ++j)
{
for(size_t k = 0; k < 3; ++k)
{
outDscale += dy.getHostValue(i, j, k, l, m)
* (x.getHostValue(i, j, k, l, m) - mean.getHostValue(i, j, k))
* rstd.getHostValue(i, j, k);
outDbias += dy.getHostValue(i, j, k, l, m);
}
}
}
EXPECT_NEAR(outDscale, dscale.getHostValue(l, m), tolerance);
EXPECT_NEAR(outDbias, dbias.getHostValue(l, m), tolerance);
}
}
}