diff --git a/.gitignore b/.gitignore index 57c1251..87f4e46 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,11 @@ __pycache__/ .harness/config.json .harness/build/ .harness/build-ninja-msvc-debug/ +.harness/build-ninja-msvc-debug-english/ +.harness/build-ninja-msvc-debug-prefix/ +.harness/build-ninja-msvc-debug-lazy/ +.harness/build-ninja-msvc-release-lazy/ +.harness/build-ninja-msvc-release-prefix-cl/ .harness/build-ninja-msvc-release/ .harness/performance/ .harness/doxygen/ diff --git a/docs/calculation-performance/mkl-coverage.md b/docs/calculation-performance/mkl-coverage.md index f02cccc..57e92ae 100644 --- a/docs/calculation-performance/mkl-coverage.md +++ b/docs/calculation-performance/mkl-coverage.md @@ -151,6 +151,26 @@ T1의 실제 Hex8 VTune stack은 `TwoSum → Add → Multiply → hex8_high_low: TwoProduct의 least-bit-sum < −1074 거부와 FMA residual·TwoSum 정규화는 그대로다. - zero/low-zero shortcut, nonzero multiplication association 변경은 채택하지 않았다. +이후 공통 산술 담당자는 실제 `fpclassify` 비용을 근거로 이 private header의 +finite 분류만 보강했다. `IsFiniteBinary64`는 memcpy로 읽은 binary64 exponent가 +2047인지 확인하며 NaN/Inf를 거부한다. 연산식·검사 위치·정규화·FMA·exact-tail +기준은 그대로다. [독립 source 검토](../../.harness/performance/shared-classification/source-numerical-review.md)는 +분류와 지원 환경의 값·실패 조건 보존을 확인했다. Signaling NaN의 exception flag나 +trap 관측까지 동등하다고 주장하지 않는다. 원래 분류의 새 경계 검사 9/9와 +변경 후 pair bit oracle·Hex8·paired state/kinematics 62/62가 native 0으로 통과했다. +[변경 전 기록](../../.harness/performance/shared-classification/before-native.json), +[변경 후 기록](../../.harness/performance/shared-classification/after-native.json)과 +[검사 로그](../../.harness/performance/shared-classification/after-checks.txt)를 보존한다. +이 결과는 최종 모델의 시간 목표나 전체 회귀 검사를 대체하지 않는다. + +위 검증 중 기존 MSVC/Ninja의 include prefix 인식 오류도 확인했다. 이 PC에는 +한국어 compiler resource만 있어 `VSLANG=1033`만 지정한 fresh build도 dependency가 +0개였다. 실제 출력 접두사 `참고: 포함 파일: `를 별도 로컬 CMake include로 지정한 +fresh build에서는 `hex8_high_low.cpp.obj`의 13개 dependency와 산술 header를 확인했다. +[실제 dependency](../../.harness/performance/shared-classification/after-dependencies.txt)를 보존한다. +공용 preset·compiler flags·설치 환경은 바꾸지 않았다. 기존 자료의 직접 cpp 재컴파일과 +header 변경 추적의 한계를 구별하며 최종 검증도 보정한 fresh build에서 수행한다. + [독립 수치 검토](../../.harness/performance/t2/arithmetic-numerical-review.md)는 위 두 helper proof/source 범위 PASS이며 실제 검사/속도와 전체 MKL diff 검토를 대체하지 않는다. Frozen original oracle는 pinned `b4fc1bb`의 원본이며 include guard와 diff --git a/docs/tracking/findings.md b/docs/tracking/findings.md index 171151d..a8864bf 100644 --- a/docs/tracking/findings.md +++ b/docs/tracking/findings.md @@ -211,3 +211,20 @@ - 현재 해결하지 않는 이유: 제품 배포 범위와 권리 검토는 아직 요청되지 않았으며 코드만으로 사용자의 배포 방식을 정할 수 없다. - 후속 방향: 배포를 실제 추진할 때 형식·대상·설치·검증·데이터 취급 조건을 구체화한다. + +## 기존 one-equation PARDISO 반복 호출의 간헐적 heap 실패 + +- 조건·현상(2026-10-14): 성능 작업에서 기존 static partial shell fixture의 free + 1×1 SPD 해법을 반복 호출할 때 native `0xc0000374`를 관찰했다. SpMV 없이 + PARDISO factor/solve/release만 호출한 진단에서도 재현했다. 같은 입력·진단 소스를 + 개선 전 Release library에 연결한 기본 8-thread 실행은 62번째 호출에서 종료했다. + [입력·runtime·실행 근거](../calculation-performance/mkl-coverage.md#baseline-pardiso-heap-failure와-제한)를 보존한다. +- 영향: 새 MKL SpMV 회귀와 분리된 기존 backend 노출이지만 vendor 내부 원인은 + 미확정이다. 반복 검사나 실제 실행의 crash를 정상 종료 또는 검사 통과로 표시하지 않는다. +- 현재 처리와 한계: 기본 정책의 1×1 PARDISO 호출에 한해 local 1-thread를 적용하고 + 호출 뒤 TLS를 복원했다. 독립 검토와 관련 lifecycle·동시성·크기 전환 검사는 통과했다. + 보조 반복 100회 통과가 라이브러리 전체 문제 해결을 보장하지 않으며, 호출자의 + 비영 TLS override와 더 큰 시스템에는 기존 정책을 유지한다. +- 후속 방향: 최종 전체 Debug 검사와 원본 결과에서 새 실패를 별도로 확인한다. + 재발 시 실제 입력·phase·thread·DLL hash와 native crash 자료를 유지하여 backend를 + 조사한다. 원인 근거 없이 dispatch·pivot·수치 기준이나 설치 환경을 바꾸지 않는다. diff --git a/src/fesa/math/high_low_arithmetic.h b/src/fesa/math/high_low_arithmetic.h index 05ccad6..b64c9de 100644 --- a/src/fesa/math/high_low_arithmetic.h +++ b/src/fesa/math/high_low_arithmetic.h @@ -38,9 +38,18 @@ struct HighLow { inline HighLow InvalidHighLow() { return {std::numeric_limits::quiet_NaN(), 0}; } +/// @brief Classifies finite binary64 values without floating arithmetic. +inline bool IsFiniteBinary64(double value) { + static_assert(sizeof(double) == sizeof(std::uint64_t) && + std::numeric_limits::is_iec559 && + std::numeric_limits::digits == 53); + std::uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & 0x7ff0000000000000ULL) != 0x7ff0000000000000ULL; +} /// @brief Checks both physical components, before comparisons or publication. inline bool IsFinite(HighLow value) { - return std::isfinite(value.high) && std::isfinite(value.low); + return IsFiniteBinary64(value.high) && IsFiniteBinary64(value.low); } /// @brief Recovers the exact error of supported finite binary64 addition. inline HighLow TwoSum(double left, double right) { @@ -48,8 +57,8 @@ inline HighLow TwoSum(double left, double right) { const double bridge = sum - left; const double restored_left = sum - bridge; const double error = (left - restored_left) + (right - bridge); - if (!std::isfinite(sum) || !std::isfinite(bridge) || - !std::isfinite(restored_left) || !std::isfinite(error)) + if (!IsFiniteBinary64(sum) || !IsFiniteBinary64(bridge) || + !IsFiniteBinary64(restored_left) || !IsFiniteBinary64(error)) return InvalidHighLow(); return sum == 0 && error == 0 ? HighLow{} : HighLow{sum, error}; } @@ -76,7 +85,7 @@ inline HighLow Add(HighLow left, HighLow right) { const auto joined = TwoSum(head.high, middle.high); const double remainder = (joined.low + middle.low) + tail.low; if (!IsFinite(head) || !IsFinite(tail) || !IsFinite(middle) || - !IsFinite(joined) || !std::isfinite(remainder)) + !IsFinite(joined) || !IsFiniteBinary64(remainder)) return InvalidHighLow(); return TwoSum(joined.high, remainder); } @@ -119,14 +128,14 @@ inline int LowestBitExponent(double value) { /// @brief Gives a product and its FMA residual when the dyadic tail fits. /// @note Rejects underflow-risk exact tails, even if the rounded high is finite. inline HighLow TwoProduct(double left, double right) { - if (!std::isfinite(left) || !std::isfinite(right)) return InvalidHighLow(); + if (!IsFiniteBinary64(left) || !IsFiniteBinary64(right)) return InvalidHighLow(); if (left == 0 || right == 0) return {}; if (LowestBitExponent(left) + LowestBitExponent(right) < -1074) return InvalidHighLow(); const double product = left * right; - if (!std::isfinite(product)) return InvalidHighLow(); + if (!IsFiniteBinary64(product)) return InvalidHighLow(); const double error = std::fma(left, right, -product); - if (!std::isfinite(error)) return InvalidHighLow(); + if (!IsFiniteBinary64(error)) return InvalidHighLow(); return TwoSum(product, error); } /// @brief Retains all four component products in deterministic order. @@ -159,13 +168,13 @@ inline HighLow Divide(HighLow numerator, HighLow denominator) { return InvalidHighLow(); if (Compare(numerator, HighLow{}) == 0) return {}; const double first = numerator.high / denominator.high; - if (!std::isfinite(first) || first == 0) return InvalidHighLow(); + if (!IsFiniteBinary64(first) || first == 0) return InvalidHighLow(); auto quotient = HighLow{first}; for (int iteration = 0; iteration < 2; ++iteration) { const auto residual = Subtract(numerator, Multiply(denominator, quotient)); if (!IsNormalized(residual)) return InvalidHighLow(); const double correction = residual.high / denominator.high; - if (!std::isfinite(correction) || (residual.high != 0 && correction == 0)) + if (!IsFiniteBinary64(correction) || (residual.high != 0 && correction == 0)) return InvalidHighLow(); quotient = Add(quotient, HighLow{correction}); } diff --git a/tests/unit/math/high_low_arithmetic_test.cpp b/tests/unit/math/high_low_arithmetic_test.cpp index 03a94ec..fc41fad 100644 --- a/tests/unit/math/high_low_arithmetic_test.cpp +++ b/tests/unit/math/high_low_arithmetic_test.cpp @@ -144,6 +144,23 @@ double FromBits(const std::uint64_t bits) { return result; } +TEST(HighLowArithmetic, FiniteClassificationMatchesBinary64Boundaries) { + const auto check = [](const double value) { + const bool expected = std::isfinite(value); + EXPECT_EQ(IsFinite(HighLow{value, 0.0}), expected); + EXPECT_EQ(IsFinite(HighLow{1.0, value}), expected); + }; + for (std::uint64_t exponent = 0; exponent < 2048U; ++exponent) { + for (const auto fraction : {0ULL, 1ULL, 1ULL << 51U, + (1ULL << 52U) - 1ULL}) { + for (const auto sign : {0ULL, 1ULL << 63U}) + check(FromBits(sign | (exponent << 52U) | fraction)); + } + } + std::mt19937_64 random{0x5800f17U}; + for (int sample = 0; sample < 10000; ++sample) check(FromBits(random())); +} + void ExpectSamePair(const HighLow actual, const fesa::high_low_original::HighLow expected) { EXPECT_EQ(Bits(actual.high), Bits(expected.high));