perf(elements): reuse masked Jet product outer transpose
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
#include "fesa/elements/nonlinear_mitc4_shell.h"
|
#include "fesa/elements/nonlinear_mitc4_shell.h"
|
||||||
|
|
||||||
|
#include <xmmintrin.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -412,11 +414,14 @@ class Hessian {
|
|||||||
|
|
||||||
/// @brief Forms an ordinary derivative outer product without a Matrix copy.
|
/// @brief Forms an ordinary derivative outer product without a Matrix copy.
|
||||||
/// @note Rejected nonfinite operands propagate to the existing Jet finite gate.
|
/// @note Rejected nonfinite operands propagate to the existing Jet finite gate.
|
||||||
void Outer(const std::array<double, kGlobalDofCount>& left,
|
bool Outer(const std::array<double, kGlobalDofCount>& left,
|
||||||
const std::array<double, kGlobalDofCount>& right, double* result) {
|
const std::array<double, kGlobalDofCount>& right, double* result) {
|
||||||
if (!dense_blas_internal::OuterProduct(left.data(), left.size(), right.data(),
|
if (!dense_blas_internal::OuterProduct(left.data(), left.size(), right.data(),
|
||||||
right.size(), result))
|
right.size(), result)) {
|
||||||
std::fill_n(result, kHessianSize, std::numeric_limits<double>::quiet_NaN());
|
std::fill_n(result, kHessianSize, std::numeric_limits<double>::quiet_NaN());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @brief Carries exact first and second trial-coordinate variations.
|
/// @brief Carries exact first and second trial-coordinate variations.
|
||||||
@@ -559,7 +564,39 @@ Jet operator*(const Jet& left, const Jet& right) {
|
|||||||
left.value * right_second[index];
|
left.value * right_second[index];
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
Outer(left.first, right.first, result_second);
|
const bool masked_exceptions =
|
||||||
|
(_mm_getcsr() & _MM_MASK_MASK) == _MM_MASK_MASK;
|
||||||
|
const bool outer_accepted = Outer(left.first, right.first, result_second);
|
||||||
|
if (masked_exceptions && outer_accepted) {
|
||||||
|
// Retain earlier upper entries for later lower entries before overwriting
|
||||||
|
// them. Reuse the MKL k=1 transpose while preserving the original row-major
|
||||||
|
// arithmetic order as well as each entry's four-term association.
|
||||||
|
std::array<double, kGlobalDofCount * (kGlobalDofCount - 1U) / 2U> upper;
|
||||||
|
std::size_t saved = 0U;
|
||||||
|
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
|
||||||
|
for (std::size_t column = 0U; column < row; ++column) {
|
||||||
|
const std::size_t index = row * kGlobalDofCount + column;
|
||||||
|
result_second[index] = left_second[index] * right.value +
|
||||||
|
result_second[index] + upper[saved++] +
|
||||||
|
left.value * right_second[index];
|
||||||
|
}
|
||||||
|
const std::size_t diagonal = row * kGlobalDofCount + row;
|
||||||
|
const double product = result_second[diagonal];
|
||||||
|
result_second[diagonal] = left_second[diagonal] * right.value +
|
||||||
|
product + product + left.value * right_second[diagonal];
|
||||||
|
for (std::size_t column = row + 1U; column < kGlobalDofCount; ++column) {
|
||||||
|
const std::size_t index = row * kGlobalDofCount + column;
|
||||||
|
const std::size_t transpose = column * kGlobalDofCount + row;
|
||||||
|
const double forward = result_second[index];
|
||||||
|
const double reverse = result_second[transpose];
|
||||||
|
upper[column * (column - 1U) / 2U + row] = forward;
|
||||||
|
result_second[index] = left_second[index] * right.value +
|
||||||
|
forward + reverse + left.value * right_second[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Preserve the reverse input-validation order after a rejected first outer.
|
||||||
std::array<double, kHessianSize> reverse;
|
std::array<double, kHessianSize> reverse;
|
||||||
Outer(right.first, left.first, reverse.data());
|
Outer(right.first, left.first, reverse.data());
|
||||||
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
|
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
|
||||||
|
|||||||
@@ -320,6 +320,154 @@ std::string Verify(bool corrupt_oracle) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Separate supplemental evidence: never changes the original T1 snapshot.
|
||||||
|
// A changed Hessian association, a lost transpose term, or a changed MKL zero
|
||||||
|
// convention must fail the bit/classification and complete FP-state comparison.
|
||||||
|
std::string ProductSnapshots(bool omit_transpose) {
|
||||||
|
const std::array<double, 16> boundary{
|
||||||
|
0.0, -0.0, 1.0, -1.0, 0.1, -0.3,
|
||||||
|
std::numeric_limits<double>::denorm_min(),
|
||||||
|
-std::numeric_limits<double>::denorm_min(),
|
||||||
|
std::numeric_limits<double>::min(),
|
||||||
|
std::nextafter(std::numeric_limits<double>::min(), 0.0),
|
||||||
|
std::numeric_limits<double>::max(),
|
||||||
|
-std::numeric_limits<double>::max(),
|
||||||
|
std::numeric_limits<double>::infinity(),
|
||||||
|
-std::numeric_limits<double>::infinity(),
|
||||||
|
std::numeric_limits<double>::quiet_NaN(),
|
||||||
|
std::numeric_limits<double>::signaling_NaN()};
|
||||||
|
std::string out;
|
||||||
|
for (const int rounding : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD,
|
||||||
|
FE_TOWARDZERO}) {
|
||||||
|
for (const unsigned flush : {0u, 0x40u, 0x8000u, 0x8040u}) {
|
||||||
|
for (const bool sticky : {false, true}) {
|
||||||
|
auto reset = [&] {
|
||||||
|
ResetFp();
|
||||||
|
std::fesetround(rounding);
|
||||||
|
_mm_setcsr((_mm_getcsr() & ~0x8040u) | flush);
|
||||||
|
if (sticky) std::feraiseexcept(FE_ALL_EXCEPT);
|
||||||
|
return FpState();
|
||||||
|
};
|
||||||
|
for (std::size_t kind = 0; kind < 96; ++kind) {
|
||||||
|
ResetFp(); // Construct inputs outside the observed environment.
|
||||||
|
Jet left{}, right{};
|
||||||
|
left.value = 0.75;
|
||||||
|
right.value = -1.25;
|
||||||
|
left.directional_first = 0.25;
|
||||||
|
right.directional_first = -0.5;
|
||||||
|
left.directional_second = -0.125;
|
||||||
|
right.directional_second = 0.375;
|
||||||
|
std::uint64_t random = 0x719b245af51d6723ULL + kind;
|
||||||
|
auto coefficient = [&] {
|
||||||
|
random ^= random << 13;
|
||||||
|
random ^= random >> 7;
|
||||||
|
random ^= random << 17;
|
||||||
|
return (static_cast<double>(random % 2000001) - 1000000) /
|
||||||
|
1048576.0;
|
||||||
|
};
|
||||||
|
for (std::size_t i = 0; i < 24; ++i) {
|
||||||
|
left.first[i] = kind < 32 ? boundary[(kind + i) % 16]
|
||||||
|
: coefficient();
|
||||||
|
right.first[i] = kind < 32 ? boundary[(kind / 2 + 3 * i) % 16]
|
||||||
|
: coefficient();
|
||||||
|
left.mixed_first[i] = coefficient();
|
||||||
|
right.mixed_first[i] = coefficient();
|
||||||
|
left.mixed_second[i] = coefficient();
|
||||||
|
right.mixed_second[i] = coefficient();
|
||||||
|
}
|
||||||
|
// Include dense, asymmetric inputs, implicit zeros and aliased input.
|
||||||
|
if (kind % 3 != 0) {
|
||||||
|
double* l = left.second.OverwriteData();
|
||||||
|
double* r = right.second.OverwriteData();
|
||||||
|
for (std::size_t i = 0; i < 576; ++i) {
|
||||||
|
l[i] = coefficient();
|
||||||
|
r[i] = coefficient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kind == 32) left = Constant(-0.0);
|
||||||
|
if (kind == 33) right = Constant(0.0);
|
||||||
|
if (kind == 34) left.value = boundary[10];
|
||||||
|
if (kind == 35) right.value = boundary[12];
|
||||||
|
if (kind == 36) left.second.Data()[17] = boundary[10];
|
||||||
|
if (kind == 37) right.second.Data()[24] = boundary[10];
|
||||||
|
if (kind == 38) left.second.Data()[17] = boundary[14];
|
||||||
|
if (kind == 39) right = left; // Shared immutable Hessian storage.
|
||||||
|
if (kind == 40) {
|
||||||
|
left.first[0] = boundary[12];
|
||||||
|
right.first[0] = boundary[15];
|
||||||
|
}
|
||||||
|
if (kind == 41) {
|
||||||
|
left.first[0] = boundary[15];
|
||||||
|
right.first[0] = boundary[12];
|
||||||
|
}
|
||||||
|
if (kind >= 42 && kind < 64) {
|
||||||
|
for (std::size_t i = 0; i < 24; ++i) {
|
||||||
|
left.first[i] = boundary[(kind + i) % 12];
|
||||||
|
right.first[i] = boundary[(kind / 2 + 3 * i) % 12];
|
||||||
|
if (kind == 42) {
|
||||||
|
left.first[i] = boundary[6];
|
||||||
|
right.first[i] = i % 2 ? -0.5 : 0.5;
|
||||||
|
}
|
||||||
|
if (kind == 43) {
|
||||||
|
left.first[i] = boundary[10];
|
||||||
|
right.first[i] = i % 2 ? -2.0 : 2.0;
|
||||||
|
}
|
||||||
|
if (kind == 44) {
|
||||||
|
left.first[i] = i % 2 ? -0.0 : 0.0;
|
||||||
|
right.first[i] = i % 3 ? -1.0 : 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto left_before = Snapshot("input", left, 0, 0, 0);
|
||||||
|
const auto right_before = Snapshot("input", right, 0, 0, 0);
|
||||||
|
const auto before = reset();
|
||||||
|
Jet product = left * right;
|
||||||
|
if (omit_transpose && kind >= 64) {
|
||||||
|
// Deliberately omit the reverse outer term in one finite entry.
|
||||||
|
// This mutant tests arithmetic detection, not just file integrity.
|
||||||
|
product.second.Data()[1] =
|
||||||
|
left.second.ReadData()[1] * right.value +
|
||||||
|
left.first[0] * right.first[1] +
|
||||||
|
left.value * right.second.ReadData()[1];
|
||||||
|
}
|
||||||
|
const int exceptions = std::fetestexcept(FE_ALL_EXCEPT);
|
||||||
|
const unsigned status = _statusfp(), mxcsr = _mm_getcsr();
|
||||||
|
const auto after = FpState();
|
||||||
|
out += "product " + std::to_string(kind) + " initial-fp=" + before +
|
||||||
|
" final-fp=" + after + " " +
|
||||||
|
Snapshot("result", product, exceptions, status, mxcsr);
|
||||||
|
if (left_before != Snapshot("input", left, 0, 0, 0) ||
|
||||||
|
right_before != Snapshot("input", right, 0, 0, 0))
|
||||||
|
throw std::runtime_error("supplemental product modified input");
|
||||||
|
|
||||||
|
// Independently pin the actual MKL operand-swap/transpose contract.
|
||||||
|
// Capture FP state before inspecting values (sNaN classification can
|
||||||
|
// itself raise INVALID). Each call starts from the same environment.
|
||||||
|
Jet forward{}, reverse{};
|
||||||
|
reset();
|
||||||
|
Outer(left.first, right.first, forward.second.OverwriteData());
|
||||||
|
const auto forward_fp = FpState();
|
||||||
|
reset();
|
||||||
|
Outer(right.first, left.first, reverse.second.OverwriteData());
|
||||||
|
const auto reverse_fp = FpState();
|
||||||
|
for (std::size_t row = 0; row < 24; ++row) {
|
||||||
|
for (std::size_t column = 0; column < 24; ++column) {
|
||||||
|
const double a = forward.second.ReadData()[row * 24 + column];
|
||||||
|
const double b = reverse.second.ReadData()[column * 24 + row];
|
||||||
|
if (!(std::isnan(a) && std::isnan(b)) && Bits(a) != Bits(b))
|
||||||
|
throw std::runtime_error("outer transpose coefficient mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += "forward-outer-fp=" + forward_fp +
|
||||||
|
" reverse-outer-fp=" + reverse_fp + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResetFp();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// Public request-mode evidence for later direction-channel separation. Existing
|
// Public request-mode evidence for later direction-channel separation. Existing
|
||||||
// tangent tests check nonzero-direction derivatives but do not freeze the full
|
// tangent tests check nonzero-direction derivatives but do not freeze the full
|
||||||
// omitted-direction response inventory and invalid-direction diagnostics.
|
// omitted-direction response inventory and invalid-direction diagnostics.
|
||||||
@@ -523,6 +671,29 @@ int main(int argc, char** argv) {
|
|||||||
fesa::Bench();
|
fesa::Bench();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
if (argc >= 3 && (std::string(argv[1]) == "--product-write" ||
|
||||||
|
std::string(argv[1]) == "--product-compare")) {
|
||||||
|
auto actual = fesa::ProductSnapshots(
|
||||||
|
argc > 3 && std::string(argv[3]) == "--omit-transpose");
|
||||||
|
if (argc > 3 && std::string(argv[3]) == "--mutate")
|
||||||
|
actual[actual.find('\n') - 1] ^= 1;
|
||||||
|
if (std::string(argv[1]) == "--product-write") {
|
||||||
|
if (std::ifstream(argv[2], std::ios::binary))
|
||||||
|
throw std::runtime_error("supplemental snapshot already exists");
|
||||||
|
std::ofstream output(argv[2], std::ios::binary);
|
||||||
|
output << actual;
|
||||||
|
output.close();
|
||||||
|
if (!output) throw std::runtime_error("supplemental snapshot write failure");
|
||||||
|
} else {
|
||||||
|
std::ifstream input(argv[2], std::ios::binary);
|
||||||
|
const std::string expected((std::istreambuf_iterator<char>(input)), {});
|
||||||
|
if (expected != actual)
|
||||||
|
throw std::runtime_error("supplemental product snapshot mismatch");
|
||||||
|
}
|
||||||
|
std::cout << argv[1] << " passed; 3072 product cases and paired MKL outer "
|
||||||
|
"checks, 651 channels, input aliases, rounding/FTZ/DAZ/sticky\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const bool verify_only = argc == 2 && std::string(argv[1]) == "--verify";
|
const bool verify_only = argc == 2 && std::string(argv[1]) == "--verify";
|
||||||
if (argc < 3 && !verify_only)
|
if (argc < 3 && !verify_only)
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
|
|||||||
@@ -38,3 +38,30 @@ snapshot은 finite 값·signed zero·Inf 비트, NaN 분류, provenance와 impli
|
|||||||
복원한 초기 FP 환경 및 연산 뒤 FE/rounding/control word/full MXCSR를 기록한다.
|
복원한 초기 FP 환경 및 연산 뒤 FE/rounding/control word/full MXCSR를 기록한다.
|
||||||
공개 반환의 방향 요청 유무·실패 진단·회복량 identity도 포함한다. 수식의 정확성은
|
공개 반환의 방향 요청 유무·실패 진단·회복량 identity도 포함한다. 수식의 정확성은
|
||||||
독립 oracle로 검사하고, snapshot은 기준의 계산 순서와 관측 의미 보존을 검사한다.
|
독립 oracle로 검사하고, snapshot은 기준의 계산 순서와 관측 의미 보존을 검사한다.
|
||||||
|
|
||||||
|
T2 곱셈 경계는 기존 T1 snapshot과 별도 CLI로 보존한다. 원본 제품 source의 hash를
|
||||||
|
확인하고 보존한 원본 archive를 연결한 실행물에서만 새 경로에 한 번 생성한다.
|
||||||
|
후보는 `--product-compare`만 실행한다. 기존 파일을 덮어쓰거나 T1 기대값을 확장하지 않는다.
|
||||||
|
|
||||||
|
```bat
|
||||||
|
<original-fixture.exe> --product-write <new-immutable-product-snapshot>
|
||||||
|
<candidate-fixture.exe> --product-compare <immutable-product-snapshot>
|
||||||
|
<candidate-fixture.exe> --product-compare <immutable-product-snapshot> --mutate
|
||||||
|
<candidate-fixture.exe> --product-compare <immutable-product-snapshot> --omit-transpose
|
||||||
|
```
|
||||||
|
|
||||||
|
마지막 두 명령은 각각 출력 변조와 reverse 외적 항 누락의 fixture 전용 음성 대조로
|
||||||
|
exit 1이 기대값이다. 이 그룹은 96개 입력을 4 rounding × 4 FTZ/DAZ × 2 초기 sticky
|
||||||
|
환경에서 실행한 3072개 곱셈이다. 전체 651 채널, 비대칭·공유·implicit Hessian과
|
||||||
|
입력 불변성, provenance, 전체 FP 상태를 비교한다. finite 전용 ±0·subnormal·underflow·
|
||||||
|
overflow gradient와 비유한 입력 거부를 구분하며, 실제 MKL 두 외적의 operand 교환과
|
||||||
|
transpose 성분도 비교한다. 두 호출의 FP 상태는 각각 기록하므로 입력 검사 조기 종료로
|
||||||
|
차이가 있는 경우를 수치 동등성으로 숨기지 않는다. NaN payload는 비교하지 않는다.
|
||||||
|
이 그룹은 예외를 mask한 상태의 관측이다. Unmasked 환경의 최초 trap 순서는 별도
|
||||||
|
SEH/process 관측이나 생성 코드 근거가 필요하며, source의 row-major 순서만으로 보장하지 않는다.
|
||||||
|
|
||||||
|
T2의 무가드 단일 외적 후보는 unmasked 최초 INVALID/OVERFLOW 순서가 달라 비채택했다.
|
||||||
|
채택한 guarded 경로는 MXCSR의 여섯 예외가 모두 masked이고 첫 외적이 성공한 경우에만
|
||||||
|
전치 성분과 276개 상삼각 scratch를 재사용한다. Unmasked 환경과 외적 거부는 기존 두 호출,
|
||||||
|
576개 reverse 배열과 row-major 결합을 유지한다. Masked snapshot과 별도 원본/후보 SEH
|
||||||
|
관측을 함께 확인하며, 정상 경로의 임시 쓰기 감소를 전체 stack 예약 감소로 보고하지 않는다.
|
||||||
|
|||||||
Reference in New Issue
Block a user