perf(math): use checked MKL GER for rank-one products

This commit is contained in:
KOKO\Mimi
2026-10-14 19:44:29 +09:00
parent c58d499727
commit 11af92809a
3 changed files with 148 additions and 1 deletions
@@ -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와 분리하여 고정한다.
+18 -1
View File
@@ -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<std::size_t>::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,
@@ -3,14 +3,65 @@
#include <gtest/gtest.h>
#include <array>
#include <cfenv>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <limits>
#include <stdexcept>
#if defined(_M_X64) || defined(_M_IX86)
#include <xmmintrin.h>
#endif
namespace fesa::dense_blas_internal {
namespace {
template <std::size_t N>
void ExpectRankOneContract(const std::array<double, N>& left,
const std::array<double, N>& 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<double, N * N> product{};
std::array<double, N * N> original{};
product.fill(std::numeric_limits<double>::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<double, 2U> left{2.0, -3.0};
@@ -38,6 +89,13 @@ TEST(DenseBlasInternal,
std::numeric_limits<std::size_t>::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<double, 6U> left{
0.0, -0.0, std::numeric_limits<double>::denorm_min(),
std::ldexp(1.0, -500), std::numeric_limits<double>::max(),
-std::numeric_limits<double>::max()};
const std::array<double, 6U> right{
0.0, -0.0, 2.0, -2.0, std::ldexp(1.0, 500),
std::numeric_limits<double>::denorm_min()};
ExpectRankOneContract(left, right);
}
TEST(DenseBlasInternal, OuterProduct24PreservesNonzeroAndFailureBoundaries) {
std::array<double, 24U> left{};
std::array<double, 24U> right{};
for (std::size_t i = 0U; i < left.size(); ++i) {
left[i] = std::ldexp(static_cast<double>(i + 1U), -8);
right[i] = std::ldexp(-static_cast<double>(i + 2U), -10);
}
left[0U] = 0.0;
left[1U] = -0.0;
left[2U] = std::numeric_limits<double>::denorm_min();
left[3U] = -std::numeric_limits<double>::denorm_min();
left[4U] = std::ldexp(1.0, -500);
left[5U] = std::numeric_limits<double>::max();
right[0U] = -0.0;
right[1U] = 0.0;
right[2U] = std::numeric_limits<double>::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<double, 2U> left{2.0, -3.0};
std::array<double, 3U> right{4.0, -5.0, 0.25};
std::array<double, 6U> destination{1.0, -2.0, 3.0, -4.0, 5.0, -6.0};
const auto accepted = destination;
for (const double invalid : {std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::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<double, 6U> left{1, 2, 3, 4, 5, 6};
const std::array<double, 12U> right{7, 8, 9, 10, 11, 12,