diff --git a/docs/calculation-performance/t4-shell-optimization.md b/docs/calculation-performance/t4-shell-optimization.md index cdb0a92..5f0334a 100644 --- a/docs/calculation-performance/t4-shell-optimization.md +++ b/docs/calculation-performance/t4-shell-optimization.md @@ -180,3 +180,21 @@ VTune summary elapsed149.866초/CPU466.466초. Self CPU: shell Jet*85.260초, io EvaluateAssembly의 hidden_second는 인접 shell 조립 Hessian map의 일반 bilinear action이다. node map 크기를24로 제한하지 않고 map 순서대로 H_ij*v_i 및 결과*v_j를 MKL diagonal batch 두 번 계산한 뒤 기존 hidden_second/abs-scale 순서로 누적한다. mass residual scale의24x24 m(row,col)*a[col]도 MKL diagonal entry batch로 계산한 뒤 기존 row/column abs 누적 순서를 유지한다. 이 두 일반 product를 fixed3 또는 AD 예외로 묶지 않는다. Private EntryProducts는 finite pair만 backend에 전달하고 invalid/intermediate entries는 NaN으로 유지하여 원래 mass/hidden finite gate에서 실패하게 한다. Failed/partial candidate를 유효 state로 공개하지 않는다. 수정 첫 build는 mass_action pack을 잘못된 첫 Gather(a) 위치에 삽입해 native2로 실패했다. 실패 source patch와 native/log는 parent `metric-green-build-failed.patch`, `metric-green-native.json`에 보존했다. Correct ordered assembly 위치로 옮긴 frozen source `metric-coverage-source.json`에서 정식 Debug build native0/6edges, 반례 GREEN native0, 관련 short106/106 PASS(native0/36.02초)를 확인했다. Actual Ninja deps에 hht_dynamic_internal.h와 dense_blas_internal.h가 포함된다. Test106은 mixed-time fixture를 제외하며 최종 전체 Debug/CTest/reference/C08/전체 모델 검증은 여전히 T5 미실행이다. 이 수정은 후속 DGER/constant/reuse source와 분리하며 독립 source 검토를 추가로 받는다. + +## MKL DGER rank1 후보와 현재 실제 검증 + +고정 f9 profile의 GEMM/dispatch self CPU 비용을 근거로 private OuterProduct만 checked row-major cblas_dger로 전환했다. 일반 MultiplyTransposeRight GEMM, public API/vendor 경계, local TLS scope와 AD coefficient/scaled-left/누적식은 유지한다. ILP64/byte-count/empty/null 및 양 입력 전체 finite validation 성공 뒤에만 destination 전체를+0 fill하고 alpha1/inc1/lda=columns GER를 수행한다. Finite input overflow는 원래 owner gate에 남긴다. 실제 gradient/scaled-left와 fresh result/temporary output의 nonalias 수명 계약을 유지한다. + +최초 새 strict-bit 반증 fixture는 실제108중107 PASS/1failed(native8), 이후 좁은 진단도native8이었다. 36entry 중10개는 GEMM -0(bits0x8000000000000000)→GER +0뿐이며 이 제한 fixture에서 비영/subnormal/overflow 결과 차이는 없었다. 그때의 source, binary hash와 실패 로그는 parent dger-failed-source.patch/dger-failed-delivery.json/dger-diagnostic-delivery.json에 보존한다. 이번 성공을 과거 실패 자료에 소급하지 않는다. + +추가 독립 `source-numerical-review-dger-zero-sign.md`는 현재 Hessian/DrillingEnergy의 영 부호 차이가 value·first·mixed 식, finite/실패, +0 초기 ordered map/COO/global CSR 및 공개 HDF5 의미를 바꾸지 않는다는 source 근거를 확인했다. 일반 GEMM kernel의 영 부호를 새 규칙으로 대체하는 sign-xor 복원은 만들지 않았다. Root 승인에 따라 owner가 이번에 작성한 반증 fixture만 실제 ordinary Hessian 계약과 구분해 비영 strict bits, 양쪽 numeric zero 값 동등 및 zero-sign 차이 명시 기록으로 조정했다. 기존 PRD/TOLERANCE·physics·pair arithmetic 기대값은 변경하지 않았다. + +Current dger-contract-source.json manifestSHA7519444546C57A81F787DFCD21230081A093B02299B43DE8C60E571FA04405B4는 source2파일 및 fixture3파일을 고정한다. Production source SHA77748F73D8F4EECF40D658FC930016A7A6A3E640FE8BC3C055178266C8229E3D는 strict-bit 실패 당시와 동일하다. 정식 Debug rebuild native0와 관련109+기존serial/TBB/repeat 결정론1을 합친 **actual110/110 PASS(native0/98.21초)**를 확인했다. Successful CTest output도 dger-contract-ctest-last.log에 보존한다. Actual6x6 zero-sign 차이10, actual24x24 차이65를 기록했고 비영 strict bits·입력 bits·rounding/MXCSR control mode 검사는 통과했다. Sticky arithmetic exception status는 원래 연산도 발생시키므로 control mode와 구분한다. + +동일 current source Release library 정식10edges build0, 생산 warning policy /W4 /WX /externalW0 standalone compile0, micro native0이다. 오래된 object/library를 복사하지 않고 actual production TU와 동일 source library를 링크했다. Frozen8c original, frozenf9 GEMM shadow route, current DGER를 동일 TU에서 비교한다. f9 Jet 계수/storage/연산식은 source span 그대로이며 OuterProduct 호출1곳만 변경되지 않은 checked MultiplyTransposeRight(inner1) GEMM wrapper로 명시적 shadow routing한다. This helper fixture는 전체 f9 executable 재현이 아니다. + +3000iterations 각0warmup/1batch 측정 결과(seconds): original0.0025600, f9GEMMdefault0.0043713, DGERdefault0.0031520, f9GEMMscope1 0.0029526, DGERscope1 0.0030267. 같은 checksum32.759844928979874, 모든 coefficient 일치/TLS 복원 확인. DGER default는 f9 default보다 약27.9% 짧지만 실제 production local scope1에서는 약2.5% 길고 original보다 약18.2% 길다. **현재 production scope 비용 개선을 입증하지 못했다.** 단일 짧은 batch의 결과를 전체 모델 성능이나100초 합격으로 사용하지 않는다. + +Timing 밖 actual Jet14cases 모두 PASS: signed-zero Constant value, seeded Variable mixed, Constant×seed, Compose mixed-third, thirdInf 실패, Sqrt0 실패, Atan2 seeded mixed, +/-0 value branch, zero/zero 실패, x2^300/y0 radius_fourthInf 수용, tiny-radius underflow 실패, zero×Infmixed 실패, allocated-zero copy. Value/first/directional/mixed 영 부호와 비영 bits는 strict 비교하고 Hessian의 양쪽 numeric zero만 허용했다. NaN payload는 결과 계약이 아니므로 양쪽 NaN 및 IsFinite 거부를 비교한다. 14case의 Hessian zero-sign 차이 수는 모두0이었다. 이 좁은 fixture는 모든 Compose/Atan2 입력·환경을 보증하지 않는다. + +Actual source/lib/exe hash 및 native argv는 parent dger-contract-native.json/dger-contract-built-hashes.json에 보존한다. Release library SHA FA4F911DEB913EAA8042A28CAA42CDADE13F5AD85715C75C537EB63F2D9DAFCA, micro exe SHA BF6DC244E6C1653F632C90ED2BCCA3DDAAB25CA9C59BBAB351336F50CC901579. 전체 shell 목표, 후속 whole-model VTune/reference/C08/최종전체Debug는 미검증이다. DGER source와 검사는 Constant specialization/reuse source와 분리하여 고정한다. diff --git a/src/fesa/math/dense_blas_internal.cpp b/src/fesa/math/dense_blas_internal.cpp index a544715..66c04b9 100644 --- a/src/fesa/math/dense_blas_internal.cpp +++ b/src/fesa/math/dense_blas_internal.cpp @@ -127,7 +127,24 @@ Matrix MultiplyTransposeLeft(const Matrix& left, const Matrix& right) { bool OuterProduct(const double* left, const std::size_t rows, const double* right, const std::size_t columns, double* destination) { - return MultiplyTransposeRight(left, rows, right, columns, 1U, destination); + const auto m = ToMklSize(rows); + const auto n = ToMklSize(columns); + const auto maximum = std::numeric_limits::max() / sizeof(double); + if (!m.HasValue() || !n.HasValue() || rows > maximum || columns > maximum || + (columns != 0U && rows > maximum / columns)) + throw std::length_error{"Dense product dimensions exceed backend limits."}; + if (rows == 0U || columns == 0U) return true; + if (destination == nullptr || left == nullptr || right == nullptr) + throw std::invalid_argument{"Dense product requires nonnull storage."}; + for (std::size_t i = 0U; i < rows; ++i) + if (!std::isfinite(left[i])) return false; + for (std::size_t i = 0U; i < columns; ++i) + if (!std::isfinite(right[i])) return false; + // GER updates A, so replace every destination entry only after validation. + std::fill_n(destination, rows * columns, 0.0); + cblas_dger(CblasRowMajor, m.Value(), n.Value(), 1.0, left, 1, right, 1, + destination, n.Value()); + return true; } bool MultiplyTransposeRight(const double* left, const std::size_t rows, diff --git a/tests/unit/math/dense_blas_internal_test.cpp b/tests/unit/math/dense_blas_internal_test.cpp index d3be2a4..55dc9c6 100644 --- a/tests/unit/math/dense_blas_internal_test.cpp +++ b/tests/unit/math/dense_blas_internal_test.cpp @@ -3,14 +3,65 @@ #include #include +#include #include #include +#include +#include +#include #include #include +#if defined(_M_X64) || defined(_M_IX86) +#include +#endif + namespace fesa::dense_blas_internal { namespace { +template +void ExpectRankOneContract(const std::array& left, + const std::array& right) { + const auto saved_left = left; + const auto saved_right = right; + const int rounding = std::fegetround(); +#if defined(_M_X64) || defined(_M_IX86) + const unsigned control = _mm_getcsr() & ~0x3fU; +#endif + std::array product{}; + std::array original{}; + product.fill(std::numeric_limits::quiet_NaN()); + const ScopedLocalDenseThreads scope{}; + ASSERT_TRUE(MultiplyTransposeRight(left.data(), N, right.data(), N, 1U, + original.data())); + ASSERT_TRUE(OuterProduct(left.data(), N, right.data(), N, product.data())); + std::size_t zero_sign_differences = 0U; + for (std::size_t i = 0U; i < product.size(); ++i) { + std::uint64_t original_bits = 0U; + std::uint64_t product_bits = 0U; + std::memcpy(&original_bits, &original[i], sizeof(original_bits)); + std::memcpy(&product_bits, &product[i], sizeof(product_bits)); + if (original[i] == 0.0 && product[i] == 0.0) { + zero_sign_differences += original_bits != product_bits ? 1U : 0U; + EXPECT_EQ(original[i], product[i]); + } else { + EXPECT_EQ(original_bits, product_bits) + << "entry=" << i << " left=" << left[i / N] + << " right=" << right[i % N] + << " original=" << original[i] << " candidate=" << product[i]; + } + } + std::cout << "DGER boundary shape=" << N << "x" << N + << " zero_sign_differences=" << zero_sign_differences << '\n'; + EXPECT_EQ(std::memcmp(saved_left.data(), left.data(), sizeof(left)), 0); + EXPECT_EQ(std::memcmp(saved_right.data(), right.data(), sizeof(right)), 0); + EXPECT_EQ(std::fegetround(), rounding); +#if defined(_M_X64) || defined(_M_IX86) + // Arithmetic may raise sticky exception flags; control bits must be retained. + EXPECT_EQ(_mm_getcsr() & ~0x3fU, control); +#endif +} + TEST(DenseBlasInternal, OuterProductUsesRectangularStorageAndPreservesRejection) { const std::array left{2.0, -3.0}; @@ -38,6 +89,13 @@ TEST(DenseBlasInternal, std::numeric_limits::max() / sizeof(double) + 1U; EXPECT_THROW(OuterProduct(nullptr, too_many_elements, nullptr, 1U, nullptr), std::length_error); + product = accepted; + EXPECT_THROW(OuterProduct(left.data(), too_large, right.data(), 1U, + product.data()), std::length_error); + EXPECT_EQ(product, accepted); + EXPECT_THROW(OuterProduct(nullptr, 2U, right.data(), 3U, product.data()), + std::invalid_argument); + EXPECT_EQ(product, accepted); } TEST(DenseBlasInternal, LocalDenseScopeRestoresTlsAfterProductsAndUnwind) { @@ -83,6 +141,60 @@ TEST(DenseBlasInternal, OuterProductLeavesFiniteInputOverflowForOwnerGate) { EXPECT_TRUE(std::isinf(product[0U])); } +TEST(DenseBlasInternal, OuterProductMatchesK1ForSignedZeroAndExtremeScale) { + const std::array left{ + 0.0, -0.0, std::numeric_limits::denorm_min(), + std::ldexp(1.0, -500), std::numeric_limits::max(), + -std::numeric_limits::max()}; + const std::array right{ + 0.0, -0.0, 2.0, -2.0, std::ldexp(1.0, 500), + std::numeric_limits::denorm_min()}; + ExpectRankOneContract(left, right); +} + +TEST(DenseBlasInternal, OuterProduct24PreservesNonzeroAndFailureBoundaries) { + std::array left{}; + std::array right{}; + for (std::size_t i = 0U; i < left.size(); ++i) { + left[i] = std::ldexp(static_cast(i + 1U), -8); + right[i] = std::ldexp(-static_cast(i + 2U), -10); + } + left[0U] = 0.0; + left[1U] = -0.0; + left[2U] = std::numeric_limits::denorm_min(); + left[3U] = -std::numeric_limits::denorm_min(); + left[4U] = std::ldexp(1.0, -500); + left[5U] = std::numeric_limits::max(); + right[0U] = -0.0; + right[1U] = 0.0; + right[2U] = std::numeric_limits::denorm_min(); + right[3U] = 2.0; + right[4U] = -2.0; + right[5U] = std::ldexp(1.0, 500); + ExpectRankOneContract(left, right); +} + +TEST(DenseBlasInternal, OuterProductRejectsEitherNonfiniteInputBeforeWriting) { + std::array left{2.0, -3.0}; + std::array right{4.0, -5.0, 0.25}; + std::array destination{1.0, -2.0, 3.0, -4.0, 5.0, -6.0}; + const auto accepted = destination; + for (const double invalid : {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}) { + right[2U] = invalid; + EXPECT_FALSE(OuterProduct(left.data(), left.size(), right.data(), + right.size(), destination.data())); + EXPECT_EQ(destination, accepted); + right[2U] = 0.25; + left[1U] = invalid; + EXPECT_FALSE(OuterProduct(left.data(), left.size(), right.data(), + right.size(), destination.data())); + EXPECT_EQ(destination, accepted); + left[1U] = -3.0; + } +} + TEST(DenseBlasInternal, TransposeRightProductUsesInnerAndRectangularStrides) { const std::array left{1, 2, 3, 4, 5, 6}; const std::array right{7, 8, 9, 10, 11, 12,