Merge reviewed guarded Hex8 packet implementation and evidence

This commit is contained in:
KOKO\Mimi
2026-10-14 21:33:19 +09:00
7 changed files with 1023 additions and 5 deletions
@@ -11,6 +11,9 @@ MKL 경계 수치 소스는 `71e08169ecfc3eee8372cd831b799b87aa47c78b`에서
후속 병합 `45aac19`에는 root의 공통 finite classifier `47d2a81`이 포함된다.
이 병합 뒤의 소비자 재빌드와 동적 개발 실행은 문서 commit `decd959`에서 수행했다.
이전 검사 결과를 새 소스의 통과로 대입하지 않는다.
후속 `3576f3f`는 root가 공통 최하위 bit 검색을 MSVC x64 intrinsic으로 바꾼
소스다. 같은 수치 header hash의 root Debug 검사와 T3 Release 실행을 아래에서
구별한다. T3가 공통 산술을 직접 수정하지 않았다.
공식 기준 Release 소스와 이 개발용 Debug 대조 소스를 혼용하지 않는다.
## 실제 병목에 대응하는 변경
@@ -249,7 +252,245 @@ EvaluateHighLow와 EvaluateSolids로 이어지는 경로가 확인된다. 여러
성능 비율로 비교하지 않는다. 공통 최하위 bit 검색의 정확한 같은 결과를 유지하면서
비용을 줄이는 후보를 root 소유 범위의 후속 검토로 전달했다.
## 공통 bit-scan 통합 뒤의 개발 실행
Root는 같은 비영 significand의 최하위 bit 위치를 `_BitScanForward64`로 찾도록
공통 private helper를 바꿨다. Exponent 구성과 exact dyadic-tail 거부 기준은
유지했다. [독립 source 검토](../../.harness/performance/shared-bitscan/source-numerical-review.md)는
header SHA `F231B9C1B0FBF963EBE1AA88D7FE4C7A291CE91C5CE4E28280AEDEB3155B7A71`의
수치·계약 범위 PASS이며 실행 검증과 구별한다. 같은 working candidate hash에서
root가 Debug 소비자 36개 edge를 재빌드하고 합성 68개 검사를 통과한
[native 기록](../../.harness/performance/shared-bitscan/after-native.json)을 보존한다.
그 뒤 root가 `3576f3f`로 commit했으며 T3는 동일 Debug 검사를 반복하지 않았다.
T3는 `3576f3f`를 동기화하고 기존 정상 dependency Release tree의 소비자 8개
edge를 재컴파일했다. Configure/build/deps 모두 native 0이며 Hex8 dependencies
15개를 확인했다. [Release 기록](../../.harness/performance/t3/bitscan-release-native.json)에
실제 argv와 소스를 남겼다. 새 binary SHA는
`54fa853d3f16ef63b0b056d1cdd18bb6b28e6e723ade3caa4b0b6b01b9d1d97e`다.
이 binary의 원본 동적 입력 개발 실행은 워밍업 없이 한 번만 수행했으며
150.1015395초 timeout이었다. Target와 측정 wrapper native는 각각 1,
execution_valid는 false이며 최종 HDF5가 없어 101-frame U 비교는 미실행이다.
[실행 자료](../../.harness/performance/t3/development-dynamic-bitscan.json),
[target 기록](../../.harness/performance/t3/development-dynamic-bitscan/run-1/native.json),
[wrapper native](../../.harness/performance/t3/dynamic-bitscan-native.json)를 보존한다.
원본/scratch 바이트와 source/header/binary/build/runtime DLL/thread/power identity가
전후 같았다. AC 연결 상태 1과 전원 scheme GUID
`8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c`를 기록했다. 종료 뒤 관련 프로세스가
없음을 확인하고 CPU 권한을 반환했다. 이번 실행 뒤 새 profile은 수집하지 않았다.
이전과 이번 실행은 모두 제한 시간에 중단되었으므로 두 elapsed로 전체 실행의
개선 비율을 계산하지 않는다. Bit-scan의 source/Debug 검증 PASS를 동적 100초
목표의 합격으로 확대하지 않는다.
## Bit-scan 소스의 남은 병목
같은 binary로 별도 승인된 software sampling 30초를 한 번 수집했다. 새 wrapper는
ProcessStartInfo의 stdout/stderr를 비동기로 분리하고 collector의 actual native를
즉시 기록했다. Collector native는 4, wrapper는 1이며 이 값을 성공으로 바꾸지
않는다. Raw result는 finalize 100%를 완료했고 기존 자료의 함수·전체 callstack·
summary export는 각각 native 0, 별도 export wrapper도 0이었다. Target native는
독립 포착하지 못해 unknown이다. 관측한 owned descendant PID 5개의 시작 ticks를
보존했으며 강제 종료 없이 모두 닫혔다. 관련 프로세스도 0개였다.
[실제 명령과 종료](../../.harness/performance/t3/vtune-dynamic-bitscan-native.json)를 보존한다.
[Summary](../../.harness/performance/t3/vtune-dynamic-bitscan-summary.csv)의 구간 elapsed는
29.859555초, CPU 227.129758초, effective 185.250598초, spin 36.140758초,
overhead 5.738402초다. 수집·finalization을 포함한 collector 경과 41.2746919초와
구별하며 어느 값도 전체 시간 목표 판정으로 사용하지 않는다.
[함수](../../.harness/performance/t3/vtune-dynamic-bitscan-functions.csv)와
[전체 호출 경로](../../.harness/performance/t3/vtune-dynamic-bitscan-callstacks.csv)는
TwoSum·Add·Multiply·MultiplyPair를 거쳐 nonlinear EvaluateHighLow로 이어지는
병목이 남음을 보여준다.
| 함수 | 보고된 self CPU초 | 제한된 core 경로의 inclusive CPU초 |
| --- | ---: | ---: |
| LowestBitExponent | 11.061455 | 11.061455 |
| TwoSum | 33.144893 | 33.144893 |
| Add | 6.402455 | 33.202178 |
| Multiply | 13.868298 | 51.911387 |
| MultiplyPair | 9.711280 | 71.850858 |
| AddPair | 4.231358 | 16.787786 |
Self는 같은 함수 이름의 export 행들을 합한 값이다. Inclusive 열은 각 blank로
구분한 stack의 첫 양수 weight 중 다른 arithmetic helper leaf만 더한 제한된
집계다. 같은 주소로 표시되는 inline finite/normalized/constructor attribution은
합산에서 제외했다. 완전한 inclusive 비용이 아니며 중첩 그룹을 서로 합산하지
않는다. [집계 근거](../../.harness/performance/t3/vtune-dynamic-bitscan-path-summary.json)에
방법과 leaf별 자료를 남겼다.
LBE/TwoSum의 독립 주소와 호출 경로는 out-of-line 경계 후보를 뒷받침하지만 실제
disassembly는 실행하지 않아 call/spill 수를 확정하지 않았다. HighLow는 16-byte
사용자 정의 pair이며 out-of-line 기본 MSVC x64 ABI에서는 간접 인자·반환 저장소가
필요하다. [Microsoft ABI 문서](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170)가
이 구조체의 호출 경계 근거다. Root가 검토할 좁은 MSVC x64 force-inline 후보는
식·guard·flags를 유지하며 실제 codegen과 개발 실행으로 효과를 확인해야 한다.
AddPair 검증 뒤의 Add와 양 low 비zero MultiplyPair 뒤의 Multiply는 normalized
입력 guard를 반복한다. 이는 source로 확인한 중복이며 inline finite attribution을
별도 비용으로 더해 절감량을 추정하지 않는다. Guard 제거보다 좁은 inline 후보를
먼저 검토하도록 전달했으며 이 단계에서 production을 수정하거나 추가 실행하지 않았다.
[Identity 자료](../../.harness/performance/t3/vtune-dynamic-bitscan-metadata.json)의 최종
before/after는 export 경계를 기록한다. 수집 전 준비 metadata는 export wrapper가
덮어썼으므로 독립 보존된 전체 수집 전후 대조로 주장하지 않는다. 최종 binary와
header/input hash는 고정된 이전 동적 실행과 같으며 이 한계를 함께 보존한다.
## 좁은 inline 선언 후보 뒤의 개발 실행
Root의 `8021dee`는 MSVC 호환 x64에서 TwoSum과 LowestBitExponent의 선언만
force-inline으로 바꾼 후보다. T3는 이를 `cb25d744`로 병합했다. 고정 arithmetic
header SHA는 `4F360FD4D11FCA68863E9FA7C084ED80E70FC4D903B9DF170AC946D19472987C`다.
Root의 [source-only 독립 검토](../../.harness/performance/shared-inline/source-numerical-review.md)와
fresh Release 205개 edge 및 같은 68개 consumer
[검사 native 0 자료](../../.harness/performance/shared-inline/after-native.json)를 보존한다.
T3는 같은 Debug 검사를 반복하지 않았다.
같은 flags·runtime·thread·power 설정으로 T3 Release 소비자 8개 edge를 재빌드했고
configure/build/deps native 0 및 Hex8 dependencies 15개를 확인했다.
[재빌드 native](../../.harness/performance/t3/inline-release-native.json)를 보존한다.
새 binary SHA는 `1532666241ab623854cdc08648fc5485371881aed9b09e1896068422185032ad`다.
원본 동적 입력을 워밍업 없이 한 번 실행한 결과는 150.0319488초 timeout이다.
Target와 측정 wrapper native는 1, execution_valid는 false이며 최종 HDF5가 없어
101-frame U 비교는 미실행이다. [실행](../../.harness/performance/t3/development-dynamic-inline.json)과
[wrapper native](../../.harness/performance/t3/dynamic-inline-native.json)를 보존한다.
이번에는 타이머 밖의 [before identity](../../.harness/performance/t3/development-dynamic-inline-before.json)와
[after identity](../../.harness/performance/t3/development-dynamic-inline-after.json)를
각각 별도 파일에 저장했다. Source/input/header/exe/DLL/build/runtime/thread/power
identity가 전후 같았고 종료 후 관련 프로세스가 없음을 확인해 CPU 권한을 반환했다.
새 profile이나 disassembly는 실행하지 않았다. Source의 annotation을 실제 모든
호출이 inline되었다는 증거로 확대하지 않는다. 이 후보도 100초 목표 미달이며,
제한 시간에 중단된 이전 후보들과의 전체 시간 개선 비율을 계산하지 않는다.
## Inline 후보의 실제 codegen과 후속 profile
별도 승인으로 같은 binary의 software sampling 30초를 한 번 수집했다. Collector
actual native 4와 outer wrapper 1을 유지한다. 기존 raw 자료만 함수·전체 callstack·
summary로 export했고 세 report와 export wrapper는 모두 native 0이다. 이번에는
collection과 export 각각의 metadata/native/before/after 파일을 분리했다.
[Collection native](../../.harness/performance/t3/vtune-dynamic-inline-collection-native.json),
[export native](../../.harness/performance/t3/vtune-dynamic-inline-export-native.json),
[collection metadata](../../.harness/performance/t3/vtune-dynamic-inline-collection-metadata.json)를
보존한다. Target native는 unknown이며 관련 owned PID는 모두 종료했다.
Source/input/header/exe/build/DLL/thread/power는 전후 같았다. 환경을 수집한 uv의
임시 Python Scripts PATH 첫 entry만 달랐으며 나머지 entry는 같았다. 실제 profiler가
상속한 parent PATH를 독립 필드로 저장하지 않았으므로 전체 runtime PATH equality는
false라는 한계를 유지한다. [Identity 검사](../../.harness/performance/t3/vtune-dynamic-inline-collection-identity-checks.json)는
이 차이를 포함하며 개발 profile을 공식 환경 합격으로 표시하지 않는다.
[Summary](../../.harness/performance/t3/vtune-dynamic-inline-summary.csv)는 elapsed
29.871824초, CPU 222.433211초, effective 173.657543초, spin 43.736681초,
overhead 5.038986초다. [함수 자료](../../.harness/performance/t3/vtune-dynamic-inline-functions.csv)의
Multiply 12.163382초, TwoProduct 11.910763초, MultiplyPair 9.367977초가 남는다.
TwoSum은 여러 inline 위치로 보고되어 다른 finite attribution의 같은 주소와
별도 비용으로 합산하지 않는다.
Hex8 paired TU 하나만 read-only dumpbin symbols/disasm으로 확인했으며 두 명령과
wrapper native는 0이다. [실제 codegen 명령](../../.harness/performance/t3/inline-codegen-native.json)과
[disassembly](../../.harness/performance/t3/inline-codegen-disasm.txt)를 보존한다.
이 TU의 direct call TwoSum/LowestBitExponent는 0개이며 TwoProduct 내부에 BSF와
TwoSum 식이 펼쳐진 명령을 확인했다. 독립 COMDAT symbol은 남아 있으므로 전체
binary의 모든 호출이 inline되었다고 주장하지 않는다. 이전 TU의 동일한 해시 object를
별도로 보존하지 않아 정확한 function-byte 크기 변화는 비교하지 않았다.
TwoProduct에는 실제 `call fma`가 남는다. Profile의 UCRT fma self는 0.975742초,
import thunk는 0.044582초이며 fma를 ancestor로 가진 다른 leaf 경로는 없었다.
단독 hardware residual dispatch는 좁지만 현재 주 병목에 대한 영향 근거가 작아
우선 후보로 선택하지 않았다. Scalar graph나 guard를 제거하는 대안으로 쓰지 않는다.
libiomp worker self 37.973383초 중 spin은 32.949078초다. Callstack은
OpenMP worker→BaseThreadInitThunk→RtlUserThreadStart만 보인다.
[FMA·spin 경로 자료](../../.harness/performance/t3/vtune-dynamic-inline-fma-spin-paths.json)는
이를 보존한다. Global PARDISO 뒤 dormant worker인지, paired TBB와 실제 시간상
겹친 resource 비용인지는 이 stack만으로 확정하지 못한다. Model을 작은 equation
system으로 가정하거나 전역 PARDISO threshold/thread 정책을 바꾸지 않았다.
`geo*point.weight`의 동일 결과를 diagonal 세 곳에서 재사용하는 작은 후보는 원래
식·누적 순서를 유지할 수 있는 별도 검토 사항으로만 남겼고 구현하지 않았다.
후속 4-column 특수 pair packet도 아직 구현·검증 전이며 이 profile의 통과 결과로
표시하지 않는다.
84개 실행과 추가 두 검사 실행은 서로 다른 검사 범위다. 이를 하나의 최종 전체
검사 실행으로 표시하지 않는다. 최종 전체 Debug/CTest, Hex8 필수 원본 5개 비교,
동적 101-frame 비교, 최종 VTune과 공식 단일 시간 측정은 아직 이 작업의 개발
검증으로 완료되지 않았다.
## 특수 pair material tangent의 4열 packet과 ISA 경계
Nonlinear Hex8의 material tangent 네 열을 독립 lane으로 계산하는 private packet을
추가했다. 일반 Float64 제품과 선형 해법은 MKL을 유지하며, packet은 normalized
high/low 특수 정밀도 예외에만 사용한다. 각 lane의 세 제품, `(first+second)+third`,
weight 제품과 기존 누적 Add 순서는 원래와 같다. Low-zero별 기존 MultiplyPair
분기 graph를 유지하며 mixed branch와 특수 입력은 원래 scalar graph로 연결한다.
Lane reduction, 대칭 복제, 새 모델 실패 gate는 추가하지 않았다.
최초 생산 `9b8e53`와 검사 `c0e23503` 후보의 Debug·Release 관련 74개 검사는
각각 native 0이었다. 실제 AVX2 선택과 4630 packet oracle, 16 uniform low-zero
fixture, 4 overflow lane 위치를 확인했다. 원본 whole-response 9804행도 일치했다.
Snapshot wrapper의 native null과 저장층 BOM/추가 newline 실패를 원본 실패 자료로
유지하고 별도 exact-stream assertion native 0으로 연결했다.
[과거 snapshot native](../../.harness/performance/t3/packet-snapshot-repaired-native.json),
[과거 exact assertion](../../.harness/performance/t3/packet-snapshot-comparison-native.json)은
새 소스의 통과로 소급하지 않는다.
최초 Release codegen에서 실제 ANDN과 AVX2 TU가 공통 inline math 실행 COMDAT를
방출하는 경계 문제를 발견해 해당 후보의 동적 전체 실행을 시작하지 않았다.
[MSVC /arch 문서](https://learn.microsoft.com/en-us/cpp/build/reference/arch-x64?view=msvc-170)는
AVX2 설정에서 BMI 등의 비벡터 명령이 생성될 수 있음을 설명한다. 지원 gate는
FMA·XSAVE·OSXSAVE·AVX·AVX2·OS XCR0와 BMI1/BMI2를 요구하고, 지원하지 않으면
기존 scalar backend를 선택한다.
보강 후보 `446d3fa059fce8141ed30d985dc8a4fa7e26a719`는 private header를 plain
C-array POD로 제한하고 공통 math/HighLow/std::array 정의를 AVX2 TU에서 제거했다.
기본 ISA out-of-line noinline 연결 함수가 checked Add/MultiplyPair/TwoProduct를
lane 0..3 순서로 호출한다. Raw high/low는 field별로 보존한다. AVX2 내부 finite·
normalized·least-bit exponent 판정은 원래 body와 guard 순서를 유지한다.
POD는 trivial carrier이며 public API·공통 math·CMake·FP flags는 이 보강에서 불변이다.
[고정 manifest](../../.harness/performance/t3/packet-boundary-446d3fa/manifest.json)와
[독립 source 검토](../../.harness/performance/t3/source-numerical-review-packet-boundary.md)를 보존한다.
Source-only PASS는 native 실행이나 ISA 검증을 대체하지 않는다.
Corrected-prefix Debug·Release build는 각각 8개 edge와 native 0이었다.
실제 discovery의 관련 76개 검사는 Debug 10.18초, Release 4.24초로 각각 전부
native 0이었다. 새 synthetic capability·frozen scalar bridge raw-bit 검사와 기존
Hex8·paired·HighLowArithmetic 검사를 포함한다. 두 binary 모두 실제 AVX2 evaluator를
선택했고 packet oracle 4630개·uniform fixture 16개·overflow 위치 4개를 확인했다.
[Debug native](../../.harness/performance/t3/packet-boundary-debug-native.json),
[Release native](../../.harness/performance/t3/packet-boundary-release-native.json),
[Debug backend](../../.harness/performance/t3/packet-boundary-debug-backend.json),
[Release backend](../../.harness/performance/t3/packet-boundary-release-backend.json)을 보존한다.
이는 내부 helper telemetry가 아니라 실제 backend 선택과 source/codegen에 연결한
구체 fixture 검사 범위다.
두 구성의 전체 symbols/disassembly 명령은 모두 native 0이었다. AVX2 객체의
정의된 external 실행 심볼은 전용 EvaluateMaterialPacketAvx2 하나이며 공통 math·
HighLow 생성자·std::array 실행 정의는 없었다. Scalar bridge는 UNDEF 외부 참조이며
상수 data와 CRT 외부 참조를 실행 정의와 구분했다. Baseline dispatch의 AVX/YMM/ANDN
명령은 0개이고 kernel fused instruction은 명시 Product residual VFMSUB 하나다.
일반 mul/add contraction은 확인되지 않았다.
[경계 assertion](../../.harness/performance/t3/packet-boundary-codegen-assertion.json)은 전체
symbol 목록과 disassembly를 연결한다. 기존 precise와 compiler flags는 유지했다.
새 Debug LIB와 정합한 현재 header로 candidate snapshot을 compile/run한 native는
각각 0이다. 성공 native 0과 original header/LIB/probe identity가 연결된 기존 old
oracle를 재사용했으며 old 실행은 반복하지 않았다. ProcessStartInfo stdout을 처음부터
BOM이나 추가 newline 없이 저장했다. 새 candidate와 원본의 raw response 9804행은
바이트 단위로 같고 SHA는 둘 다 `DED8D2C380D15E297A73BDD018B1B840C1ADC0D8372934D9FC2EF2B628A36F63`이다.
비교 assertion wrapper native 0을 [새 snapshot native](../../.harness/performance/t3/packet-boundary-snapshot-native.json)와
[새 비교](../../.harness/performance/t3/packet-boundary-snapshot-comparison.json)에 연결했다.
같은 고정 Release binary로 원본 동적 입력을 워밍업 없이 한 번 실행했다.
150.0500624초에 제한 시간으로 중단됐고 target/측정 wrapper native는 1이다.
Execution_valid는 false이고 최종 HDF5가 없어 101-frame U 비교는 미실행이다.
100초 목표는 미달이다. [개발 실행](../../.harness/performance/t3/development-dynamic-packet-boundary.json),
[wrapper native](../../.harness/performance/t3/dynamic-packet-boundary-native.json),
[before identity](../../.harness/performance/t3/development-dynamic-packet-boundary-before.json),
[after identity](../../.harness/performance/t3/development-dynamic-packet-boundary-after.json)를 보존한다.
Source/input/header/exe/DLL/build/runtime/thread/power identity가 전후 같았으며 원본과
scratch SHA도 같았다. 종료 후 관련 프로세스 0개를 확인하고 CPU 권한을 반환했다.
추가 profile/micro 또는 전체 시간 실행은 하지 않았다. 제한 시간에 중단된 후보끼리의
개선 비율을 계산하지 않는다. 최종 전체 검사·필수 원본 비교·공식 시간 목표는 미완료다.
+10
View File
@@ -44,6 +44,8 @@ add_library(
elements/element_factory.cpp
elements/hex8_element.cpp
elements/hex8_high_low.cpp
elements/hex8_pair_simd_dispatch.cpp
elements/hex8_pair_simd_avx2.cpp
elements/hex8_geometry.cpp
elements/euler_beam_3d.cpp
elements/finite_rotation.cpp
@@ -122,6 +124,14 @@ target_compile_options(
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX /EHsc>
)
# Keep the CPU/OS dispatch on the baseline ISA; only its private kernel uses AVX2.
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND
CMAKE_CXX_COMPILER_ARCHITECTURE_ID STREQUAL "x64")
target_compile_definitions(fesa_solver PRIVATE FESA_HEX8_PAIR_AVX2=1)
set_source_files_properties(elements/hex8_pair_simd_avx2.cpp
PROPERTIES COMPILE_OPTIONS /arch:AVX2)
endif()
add_executable(fesa_cli app/main.cpp)
set_target_properties(fesa_cli PROPERTIES OUTPUT_NAME fesa)
target_link_libraries(fesa_cli PRIVATE fesa_solver)
+39 -1
View File
@@ -9,6 +9,7 @@
#include "hex8_geometry.h"
#include "hex8_linear_tangent_cache.h"
#include "hex8_pair_arithmetic.h"
#include "hex8_pair_simd.h"
namespace fesa {
namespace {
@@ -163,6 +164,11 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
return Result<Hex8HighLowResponse>::Failure(
Failure("Hex8 high+low constitutive coefficients are unsupported."));
energy = HighLow{0.5} * HighLow{kappa_} * volume * mean_trace * mean_trace;
const auto packet_evaluator = nonlinear
? hex8_internal::SelectMaterialPacketEvaluator()
: hex8_internal::EvaluateMaterialPacketScalar;
const bool use_packets =
packet_evaluator != hex8_internal::EvaluateMaterialPacketScalar;
for (std::size_t g = 0; g < 8; ++g) {
const auto& point = points_[g];
const auto& gradients = point_gradients[g];
@@ -238,9 +244,11 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
for (std::size_t j = 0; j < 3; ++j)
for (std::size_t l = 0; l < 3; ++l)
gradient_stress[a][j][l] = gradients[a][j] * s[l][j];
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t a = 0; a < 8; ++a) {
std::array<HighLow, 8> dots{};
for (std::size_t b = 0; b < 8; ++b) {
const auto dot = Dot(gradients[a], gradients[b]);
dots[b] = dot;
HighLow geo;
if (nonlinear)
for (std::size_t j = 0; j < 3; ++j)
@@ -250,14 +258,44 @@ Result<Hex8HighLowResponse> Hex8Element::EvaluateHighLow(
for (std::size_t k = 0; k < 3; ++k) {
const auto row = 3U * a + i;
const auto column = 3U * b + k;
if (!use_packets) {
const auto mat = deviatoric_fg[a][i] * fg[b][k] +
shear_fg[b][i] * fg[a][k] +
shear_fft[i][k] * dot;
material[row][column] += mat * HighLow{point.weight};
}
if (i == k)
geometric[row][column] += geo * HighLow{point.weight};
}
}
if (use_packets)
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t first = 0; first < 24; first += 4) {
hex8_internal::MaterialPacketInput input{};
const auto raw_pair = [](HighLow pair) {
return hex8_internal::PacketPair{pair.high, pair.low};
};
input.weight = raw_pair(HighLow{point.weight});
const auto row = 3U * a + i;
for (std::size_t lane = 0; lane < 4; ++lane) {
const auto column = first + lane;
const auto b = column / 3U;
const auto k = column % 3U;
input.deviatoric_left.values[lane] = raw_pair(deviatoric_fg[a][i]);
input.deviatoric_right.values[lane] = raw_pair(fg[b][k]);
input.shear_left.values[lane] = raw_pair(shear_fg[b][i]);
input.shear_right.values[lane] = raw_pair(fg[a][k]);
input.tensor_left.values[lane] = raw_pair(shear_fft[i][k]);
input.gradient_dot.values[lane] = raw_pair(dots[b]);
input.accumulated.values[lane] = raw_pair(material[row][column]);
}
hex8_internal::PairPacket output;
packet_evaluator(input, &output);
for (std::size_t lane = 0; lane < 4; ++lane)
material[row][first + lane] =
{output.values[lane].high, output.values[lane].low};
}
}
}
if (nonlinear || !paired_linear_tangent_->ready) {
std::array<Triple, 8> bulk_fg{};
+60
View File
@@ -0,0 +1,60 @@
#ifndef FESA_SRC_FESA_ELEMENTS_HEX8_PAIR_SIMD_H_
#define FESA_SRC_FESA_ELEMENTS_HEX8_PAIR_SIMD_H_
#include <cstdint>
namespace fesa::hex8_internal {
/// @brief Trivial raw carrier; neither component is normalized or rounded here.
struct PacketPair {
double high;
double low;
};
/// @brief Four independent lanes with no executable constructor or operator.
struct PairPacket {
PacketPair values[4];
};
struct MaterialPacketInput {
PairPacket deviatoric_left;
PairPacket deviatoric_right;
PairPacket shear_left;
PairPacket shear_right;
PairPacket tensor_left;
PairPacket gradient_dot;
PairPacket accumulated;
PacketPair weight;
};
using MaterialPacketEvaluator = void (*)(const MaterialPacketInput&, PairPacket*);
/// @brief Evaluates each lane's original checked material graph in order 0..3.
/// @note Invalid arithmetic remains a raw invalid pair for the caller's gates.
void EvaluateMaterialPacketScalar(const MaterialPacketInput& input,
PairPacket* output);
/// @brief Evaluates independent material lanes with the checked vector graph.
/// @pre The selector has admitted the CPU/OS ISA and current masked FP state.
/// @note No lane reduction or model failure is introduced; special packets bridge
/// to original scalar arithmetic, preserving components and operation order.
void EvaluateMaterialPacketAvx2(const MaterialPacketInput& input,
PairPacket* output);
/// @brief Baseline-ISA checked addition bridge in lane order 0..3.
/// @note pair_shortcuts chooses the original local AddPair or general Add graph;
/// invalid pairs are preserved, with no FP-state mutation or normalization here.
void ScalarPacketAdd(const PairPacket& left, const PairPacket& right,
bool pair_shortcuts, PairPacket* output);
/// @brief Baseline-ISA original checked MultiplyPair bridge in lane order 0..3.
/// @note Raw invalid outputs remain invalid; no component is rounded away.
void ScalarPacketMultiply(const PairPacket& left, const PairPacket& right,
PairPacket* output);
/// @brief Baseline-ISA original checked TwoProduct bridge in lane order 0..3.
/// @note Keeps finite/zero/underflow/FMA checks and the caller's FP state.
void ScalarPacketTwoProduct(const double left[4], const double right[4],
PairPacket* output);
/// @brief Selects a private backend from CPU/OS and this thread's FP state.
/// @note Never mutates FP state or caches a thread's environment decision.
MaterialPacketEvaluator SelectMaterialPacketEvaluator();
/// @brief Pure instruction/OS-state predicate; does not execute ISA probes.
bool SupportsMaterialPacketInstructions(std::uint32_t leaf1_ecx,
std::uint64_t xcr0,
std::uint32_t leaf7_ebx);
} // namespace fesa::hex8_internal
#endif // FESA_SRC_FESA_ELEMENTS_HEX8_PAIR_SIMD_H_
+241
View File
@@ -0,0 +1,241 @@
#include "hex8_pair_simd.h"
#if defined(FESA_HEX8_PAIR_AVX2) && defined(_MSC_VER) && defined(_M_X64)
#include <cstring>
#include <immintrin.h>
#include <intrin.h>
#include <limits>
namespace fesa::hex8_internal {
namespace {
struct VectorPair {
__m256d high;
__m256d low;
};
// TU-local predicates mirror the checked scalar premises without publishing
// AVX2-compiled common math or HighLow COMDAT definitions to baseline callers.
/// @brief Reads binary64 fields without FP classification or comparison.
std::uint64_t ScalarBits(double value) {
static_assert(sizeof(double) == sizeof(std::uint64_t) &&
std::numeric_limits<double>::is_iec559 &&
std::numeric_limits<double>::digits == 53);
std::uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
/// @brief Bitwise finite gate before any scalar FP comparison.
bool FiniteScalar(double value) {
return (ScalarBits(value) & 0x7ff0000000000000ULL) !=
0x7ff0000000000000ULL;
}
/// @brief Preserves finite, canonical-zero and RN64 normalization gates.
bool NormalizedScalar(PacketPair value) {
if (!FiniteScalar(value.high) || !FiniteScalar(value.low)) return false;
if (value.high == 0 && value.low == 0)
return (ScalarBits(value.high) >> 63U) == 0 &&
(ScalarBits(value.low) >> 63U) == 0;
return value.high + value.low == value.high;
}
/// @brief Exact least-bit exponent; caller has checked finite/nonzero first.
int LowestBitExponentScalar(double value) {
const auto bits = ScalarBits(value);
const auto encoded_exponent = (bits >> 52U) & 0x7ffU;
auto significand = bits & 0xfffffffffffffULL;
const int exponent = encoded_exponent == 0U
? -1074
: static_cast<int>(encoded_exponent) - 1075;
if (encoded_exponent != 0U) significand |= 1ULL << 52U;
unsigned long trailing = 0;
_BitScanForward64(&trailing, significand);
return exponent + static_cast<int>(trailing);
}
/// @brief Selects each lane without evaluating additional FP arithmetic.
__m256d Select(__m256d mask, __m256d selected, __m256d other) {
return _mm256_blendv_pd(other, selected, mask);
}
/// @brief Produces independent finite masks without signaling NaN compares.
__m256d Finite(__m256d value) {
const auto exponent = _mm256_set1_epi64x(0x7ff0000000000000LL);
const auto nonfinite = _mm256_cmpeq_epi64(
_mm256_and_si256(_mm256_castpd_si256(value), exponent), exponent);
return _mm256_castsi256_pd(_mm256_xor_si256(
nonfinite, _mm256_set1_epi64x(-1)));
}
/// @brief Recognizes both signed zero encodings using integer fields.
__m256d Zero(__m256d value) {
return _mm256_castsi256_pd(_mm256_cmpeq_epi64(
_mm256_and_si256(_mm256_castpd_si256(value),
_mm256_set1_epi64x(0x7fffffffffffffffLL)),
_mm256_setzero_si256()));
}
/// @brief Preserves valid lanes and encodes invalid lanes as quiet NaN/+0.
VectorPair InvalidOutside(VectorPair value, __m256d valid) {
return {Select(valid, value.high, _mm256_castsi256_pd(
_mm256_set1_epi64x(0x7ff8000000000000LL))),
Select(valid, value.low, _mm256_setzero_pd())};
}
/// @brief Loads raw high/low fields in lane order 0..3, with no normalization.
VectorPair Load(const PairPacket& value) {
return {_mm256_setr_pd(value.values[0].high, value.values[1].high,
value.values[2].high, value.values[3].high),
_mm256_setr_pd(value.values[0].low, value.values[1].low,
value.values[2].low, value.values[3].low)};
}
/// @brief Stores raw high/low fields without rounding or lane reduction.
PairPacket Store(VectorPair value) {
double high[4];
double low[4];
_mm256_storeu_pd(high, value.high);
_mm256_storeu_pd(low, value.low);
return {{{high[0], low[0]}, {high[1], low[1]},
{high[2], low[2]}, {high[3], low[3]}}};
}
/// @brief Retains the original per-lane subtraction/addition association and gates.
VectorPair TwoSum(__m256d left, __m256d right) {
const auto sum = _mm256_add_pd(left, right);
const auto bridge = _mm256_sub_pd(sum, left);
const auto restored = _mm256_sub_pd(sum, bridge);
const auto error = _mm256_add_pd(_mm256_sub_pd(left, restored),
_mm256_sub_pd(right, bridge));
const auto valid = _mm256_and_pd(_mm256_and_pd(Finite(sum), Finite(bridge)),
_mm256_and_pd(Finite(restored), Finite(error)));
const auto exact_zero = _mm256_and_pd(Zero(sum), Zero(error));
return InvalidOutside({Select(exact_zero, _mm256_setzero_pd(), sum),
Select(exact_zero, _mm256_setzero_pd(), error)}, valid);
}
/// @brief Routes exceptional packets to baseline checked Add in lane order.
VectorPair ScalarAdd(VectorPair left, VectorPair right, bool pair_shortcuts) {
const auto a = Store(left);
const auto b = Store(right);
PairPacket result;
ScalarPacketAdd(a, b, pair_shortcuts, &result);
return Load(result);
}
/// @brief Routes exceptional packets to baseline checked MultiplyPair.
VectorPair ScalarMultiply(const PairPacket& left, const PairPacket& right) {
PairPacket result;
ScalarPacketMultiply(left, right, &result);
return Load(result);
}
/// @brief Retains normalized/zero branches and the checked per-lane Add graph.
VectorPair Add(VectorPair left, VectorPair right, bool pair_shortcuts) {
const auto a = Store(left);
const auto b = Store(right);
int left_zero = 0;
int right_zero = 0;
for (std::size_t lane = 0; lane < 4; ++lane) {
if (!NormalizedScalar(a.values[lane]) ||
!NormalizedScalar(b.values[lane]))
return ScalarAdd(left, right, pair_shortcuts);
if (a.values[lane].high == 0 && a.values[lane].low == 0) left_zero |= 1 << lane;
if (b.values[lane].high == 0 && b.values[lane].low == 0) right_zero |= 1 << lane;
}
if (pair_shortcuts) {
if (left_zero == 15)
return {right.high, Select(Zero(right.low), _mm256_setzero_pd(), right.low)};
if (right_zero == 15)
return {left.high, Select(Zero(left.low), _mm256_setzero_pd(), left.low)};
if (left_zero != 0 || right_zero != 0)
return ScalarAdd(left, right, true);
}
const auto head = TwoSum(left.high, right.high);
const auto tail = TwoSum(left.low, right.low);
const auto middle = TwoSum(head.low, tail.high);
const auto joined = TwoSum(head.high, middle.high);
const auto remainder = _mm256_add_pd(
_mm256_add_pd(joined.low, middle.low), tail.low);
const auto valid = _mm256_and_pd(Finite(remainder), _mm256_and_pd(
_mm256_and_pd(Finite(head.high), Finite(head.low)), _mm256_and_pd(
_mm256_and_pd(Finite(tail.high), Finite(tail.low)), _mm256_and_pd(
_mm256_and_pd(Finite(middle.high), Finite(middle.low)),
_mm256_and_pd(Finite(joined.high), Finite(joined.low))))));
// Original Add returns before its last TwoSum when a prior finite gate fails.
return InvalidOutside(TwoSum(
Select(valid, joined.high, _mm256_setzero_pd()),
Select(valid, remainder, _mm256_setzero_pd())), valid);
}
/// @brief Checks finite/zero/tail premises before products; neutralizes rejected
/// lanes before the explicit residual FMA and final TwoSum.
VectorPair Product(__m256d left, __m256d right) {
double a[4];
double b[4];
_mm256_storeu_pd(a, left);
_mm256_storeu_pd(b, right);
bool special = false;
for (std::size_t lane = 0; lane < 4; ++lane)
if (!FiniteScalar(a[lane]) ||
!FiniteScalar(b[lane]) ||
a[lane] == 0 || b[lane] == 0 ||
LowestBitExponentScalar(a[lane]) +
LowestBitExponentScalar(b[lane]) < -1074)
special = true;
if (special) {
PairPacket result;
ScalarPacketTwoProduct(a, b, &result);
return Load(result);
}
const auto product = _mm256_mul_pd(left, right);
const auto product_valid = Finite(product);
const auto one = _mm256_set1_pd(1.0);
// Invalid products never reach the residual FMA in the original helper.
const auto error = _mm256_fmsub_pd(Select(product_valid, left, one),
Select(product_valid, right, one), Select(product_valid, product, one));
const auto valid = _mm256_and_pd(product_valid, Finite(error));
return InvalidOutside(TwoSum(
Select(valid, product, _mm256_setzero_pd()),
Select(valid, error, _mm256_setzero_pd())), valid);
}
/// @brief Uses uniform low-zero branches with original four-product association;
/// mixed or invalid packets preserve the baseline checked path.
VectorPair Multiply(VectorPair left, VectorPair right) {
const auto a = Store(left);
const auto b = Store(right);
int branch = -1;
for (std::size_t lane = 0; lane < 4; ++lane) {
const bool normalized = NormalizedScalar(a.values[lane]) &&
NormalizedScalar(b.values[lane]);
if (!normalized) return ScalarMultiply(a, b);
const int shape = (a.values[lane].low != 0 ? 1 : 0) |
(b.values[lane].low != 0 ? 2 : 0);
if (branch != -1 && shape != branch) return ScalarMultiply(a, b);
branch = shape;
}
auto product = Product(left.high, right.high);
if (branch == 3) {
product = Add(product, Product(left.high, right.low), false);
product = Add(product, Product(left.low, right.high), false);
return Add(product, Product(left.low, right.low), false);
}
const VectorPair zero{_mm256_setzero_pd(), _mm256_setzero_pd()};
if ((branch & 2) != 0)
product = Add(product, Product(left.high, right.low), true);
else if ((branch & 1) != 0)
product = Add(product, zero, true);
if ((branch & 1) != 0)
product = Add(product, Product(left.low, right.high), true);
return Add(product, zero, true);
}
} // namespace
void EvaluateMaterialPacketAvx2(const MaterialPacketInput& input,
PairPacket* output) {
const auto first = Multiply(Load(input.deviatoric_left),
Load(input.deviatoric_right));
const auto second = Multiply(Load(input.shear_left), Load(input.shear_right));
const auto third = Multiply(Load(input.tensor_left), Load(input.gradient_dot));
const auto material = Add(Add(first, second, true), third, true);
const PairPacket weight{{input.weight, input.weight, input.weight, input.weight}};
*output = Store(Add(Load(input.accumulated),
Multiply(material, Load(weight)), true));
}
} // namespace fesa::hex8_internal
#else
namespace fesa::hex8_internal {
void EvaluateMaterialPacketAvx2(const MaterialPacketInput& input,
PairPacket* output) {
EvaluateMaterialPacketScalar(input, output);
}
} // namespace fesa::hex8_internal
#endif
@@ -0,0 +1,107 @@
#include "hex8_pair_simd.h"
#include "hex8_pair_arithmetic.h"
#if defined(FESA_HEX8_PAIR_AVX2) && defined(_MSC_VER) && defined(_M_X64)
#include <float.h>
#include <intrin.h>
#include <xmmintrin.h>
#endif
namespace fesa::hex8_internal {
#if defined(_MSC_VER)
#define FESA_HEX8_BASELINE_NOINLINE __declspec(noinline)
#else
#define FESA_HEX8_BASELINE_NOINLINE
#endif
FESA_HEX8_BASELINE_NOINLINE
void ScalarPacketAdd(const PairPacket& left, const PairPacket& right,
bool pair_shortcuts, PairPacket* output) {
for (std::size_t lane = 0; lane < 4; ++lane) {
const math_internal::HighLow a{left.values[lane].high, left.values[lane].low};
const math_internal::HighLow b{right.values[lane].high, right.values[lane].low};
const auto value = pair_shortcuts ? AddPair(a, b) : math_internal::Add(a, b);
output->values[lane] = {value.high, value.low};
}
}
FESA_HEX8_BASELINE_NOINLINE
void ScalarPacketMultiply(const PairPacket& left, const PairPacket& right,
PairPacket* output) {
for (std::size_t lane = 0; lane < 4; ++lane) {
const auto value = MultiplyPair(
{left.values[lane].high, left.values[lane].low},
{right.values[lane].high, right.values[lane].low});
output->values[lane] = {value.high, value.low};
}
}
FESA_HEX8_BASELINE_NOINLINE
void ScalarPacketTwoProduct(const double left[4], const double right[4],
PairPacket* output) {
for (std::size_t lane = 0; lane < 4; ++lane) {
const auto value = math_internal::TwoProduct(left[lane], right[lane]);
output->values[lane] = {value.high, value.low};
}
}
#undef FESA_HEX8_BASELINE_NOINLINE
bool SupportsMaterialPacketInstructions(std::uint32_t leaf1_ecx,
std::uint64_t xcr0,
std::uint32_t leaf7_ebx) {
constexpr std::uint32_t required_leaf1 =
(1U << 12) | (1U << 26) | (1U << 27) | (1U << 28);
// MSVC /arch:AVX2 permits BMI codegen; ANDN is present in this kernel.
constexpr std::uint32_t required_leaf7 =
(1U << 3) | (1U << 5) | (1U << 8);
return (leaf1_ecx & required_leaf1) == required_leaf1 &&
(xcr0 & 6U) == 6U &&
(leaf7_ebx & required_leaf7) == required_leaf7;
}
void EvaluateMaterialPacketScalar(const MaterialPacketInput& input,
PairPacket* output) {
const auto value = [](PacketPair pair) {
return math_internal::HighLow{pair.high, pair.low};
};
for (std::size_t lane = 0; lane < 4; ++lane) {
const auto first = MultiplyPair(value(input.deviatoric_left.values[lane]),
value(input.deviatoric_right.values[lane]));
const auto second = MultiplyPair(value(input.shear_left.values[lane]),
value(input.shear_right.values[lane]));
const auto third = MultiplyPair(value(input.tensor_left.values[lane]),
value(input.gradient_dot.values[lane]));
const auto material = AddPair(AddPair(first, second), third);
const auto accumulated = AddPair(value(input.accumulated.values[lane]),
MultiplyPair(material, value(input.weight)));
output->values[lane] = {accumulated.high, accumulated.low};
}
}
MaterialPacketEvaluator SelectMaterialPacketEvaluator() {
#if defined(FESA_HEX8_PAIR_AVX2) && defined(_MSC_VER) && defined(_M_X64)
static const bool hardware_supported = [] {
int info[4]{};
__cpuid(info, 0);
if (info[0] < 7) return false;
__cpuidex(info, 1, 0);
constexpr int required = (1 << 12) | (1 << 26) | (1 << 27) | (1 << 28);
if ((info[2] & required) != required) return false;
const auto leaf1 = static_cast<std::uint32_t>(info[2]);
const auto xcr0 = _xgetbv(0);
__cpuidex(info, 7, 0);
return SupportsMaterialPacketInstructions(
leaf1, xcr0, static_cast<std::uint32_t>(info[1]));
}();
if (!hardware_supported || !math_internal::ArithmeticEnvironmentSupported())
return EvaluateMaterialPacketScalar;
if ((_mm_getcsr() & 0x1f80U) != 0x1f80U)
return EvaluateMaterialPacketScalar;
unsigned int control = 0;
if (_controlfp_s(&control, 0, 0) != 0 ||
(control & _MCW_EM) != _MCW_EM)
return EvaluateMaterialPacketScalar;
return EvaluateMaterialPacketAvx2;
#else
return EvaluateMaterialPacketScalar;
#endif
}
} // namespace fesa::hex8_internal
+321
View File
@@ -12,6 +12,7 @@
#include <limits>
#include <random>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
@@ -19,6 +20,7 @@
#include "../../../src/fesa/math/high_low_arithmetic.h"
#include "../../../src/fesa/elements/hex8_geometry.h"
#include "../../../src/fesa/elements/hex8_pair_arithmetic.h"
#include "../../../src/fesa/elements/hex8_pair_simd.h"
#include "../math/high_low_arithmetic_original.h"
namespace {
@@ -84,6 +86,325 @@ TEST(Hex8HighLow, ScalarCrossProductShortcutMatchesOriginalBits) {
}
}
fesa::hex8_internal::PacketPair RawPacketPair(HighLow value) {
return {value.high, value.low};
}
fesa::hex8_internal::PairPacket FrozenMaterialPacket(
const fesa::hex8_internal::MaterialPacketInput& input) {
using Original = fesa::high_low_original::HighLow;
const auto original = [](fesa::hex8_internal::PacketPair value) {
return Original{value.high, value.low};
};
fesa::hex8_internal::PairPacket result;
for (std::size_t lane = 0; lane < 4; ++lane) {
const auto first = fesa::high_low_original::Multiply(
original(input.deviatoric_left.values[lane]),
original(input.deviatoric_right.values[lane]));
const auto second = fesa::high_low_original::Multiply(
original(input.shear_left.values[lane]), original(input.shear_right.values[lane]));
const auto third = fesa::high_low_original::Multiply(
original(input.tensor_left.values[lane]), original(input.gradient_dot.values[lane]));
const auto value = fesa::high_low_original::Add(
fesa::high_low_original::Add(first, second), third);
const auto accumulated = fesa::high_low_original::Add(
original(input.accumulated.values[lane]),
fesa::high_low_original::Multiply(value, original(input.weight)));
result.values[lane] = {accumulated.high, accumulated.low};
}
return result;
}
bool ExpectFrozenMaterialPacket(
const fesa::hex8_internal::MaterialPacketInput& input) {
const auto expected = FrozenMaterialPacket(input);
fesa::hex8_internal::PairPacket scalar, selected;
fesa::hex8_internal::EvaluateMaterialPacketScalar(input, &scalar);
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
evaluator(input, &selected);
for (std::size_t lane = 0; lane < 4; ++lane) {
SCOPED_TRACE(lane);
EXPECT_EQ(PairBits(scalar.values[lane].high), PairBits(expected.values[lane].high));
EXPECT_EQ(PairBits(scalar.values[lane].low), PairBits(expected.values[lane].low));
EXPECT_EQ(PairBits(selected.values[lane].high), PairBits(expected.values[lane].high));
EXPECT_EQ(PairBits(selected.values[lane].low), PairBits(expected.values[lane].low));
}
return evaluator != fesa::hex8_internal::EvaluateMaterialPacketScalar;
}
TEST(Hex8HighLow, FourColumnMaterialPacketMatchesFrozenScalarBits) {
ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported());
using fesa::hex8_internal::MaterialPacketInput;
RecordProperty("private_packet_backend",
fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
fesa::hex8_internal::EvaluateMaterialPacketScalar ? "scalar" : "avx2");
std::size_t packet_checks = 0, avx2_evaluator_checks = 0;
const double tiny = std::numeric_limits<double>::denorm_min();
const double maximum = std::numeric_limits<double>::max();
const std::vector<HighLow> boundary{{}, {-0.0, 0}, {0, -0.0},
{1}, {-1}, {1, -0.0}, {1, std::ldexp(1.0, -54)},
{-1, std::ldexp(1.0, -54)}, {tiny}, {-tiny},
{std::numeric_limits<double>::min()}, {maximum}, {-maximum},
{1, tiny}, {1, -tiny}, {std::nextafter(1.0, 2.0)},
{1, 1}, {0, tiny}, {std::numeric_limits<double>::infinity(), 0},
{std::numeric_limits<double>::quiet_NaN(), 0},
{std::numeric_limits<double>::signaling_NaN(), 0},
{0, std::numeric_limits<double>::signaling_NaN()}};
for (std::size_t first = 0; first < boundary.size(); ++first)
for (std::size_t second = 0; second < boundary.size(); ++second) {
MaterialPacketInput input{};
input.weight = {0.125};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = RawPacketPair(boundary[first]);
input.deviatoric_right.values[lane] = RawPacketPair(boundary[second]);
input.shear_left.values[lane] = {2, std::ldexp(1.0, -54)};
input.shear_right.values[lane] = {-0.5, std::ldexp(1.0, -56)};
input.tensor_left.values[lane] = {0.75};
input.gradient_dot.values[lane] = {-0.25};
input.accumulated.values[lane] = lane == 0 ? fesa::hex8_internal::PacketPair{0, 0}
: fesa::hex8_internal::PacketPair{0.125, 0};
}
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
++packet_checks;
}
for (int exponent = -1074; exponent <= 1023; ++exponent) {
MaterialPacketInput input{};
input.weight = {1};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {
std::ldexp(lane % 2U == 0 ? 1.0 : -1.0, exponent)};
input.deviatoric_right.values[lane] = {lane < 2 ? 0.5 : 2.0};
}
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
++packet_checks;
}
std::mt19937_64 random(0x4c0116U);
for (std::size_t sample = 0; sample < 2048; ++sample) {
MaterialPacketInput input{};
const auto pair = [&]() {
const double high = std::ldexp(
1.0 + static_cast<double>(random() % 256U) / 256.0,
static_cast<int>(random() % 201U) - 100);
const double signed_high = (random() & 1U) != 0 ? high : -high;
const double low = sample % 3U == 0 ? 0.0 : std::ldexp(high, -55);
return fesa::hex8_internal::PacketPair{
signed_high, (random() & 1U) != 0 ? low : -low};
};
input.weight = sample % 2U == 0
? fesa::hex8_internal::PacketPair{0.125, 0} : pair();
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = pair();
input.deviatoric_right.values[lane] = pair();
input.shear_left.values[lane] = pair();
input.shear_right.values[lane] = pair();
input.tensor_left.values[lane] = pair();
input.gradient_dot.values[lane] = pair();
input.accumulated.values[lane] = sample % 4U == 0
? fesa::hex8_internal::PacketPair{0, 0} : pair();
}
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
++packet_checks;
}
RecordProperty("packet_oracle_checks", static_cast<int>(packet_checks));
RecordProperty("avx2_evaluator_packet_checks",
static_cast<int>(avx2_evaluator_checks));
}
TEST(Hex8HighLow, PacketSpecialLaneDoesNotChangeOtherColumns) {
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {0.125};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)};
input.deviatoric_right.values[lane] = {2, std::ldexp(1.0, -55)};
input.shear_left.values[lane] = {0.5};
input.shear_right.values[lane] = {-0.25};
input.tensor_left.values[lane] = {0.75};
input.gradient_dot.values[lane] = {0.125};
}
const auto reference = FrozenMaterialPacket(input);
const std::vector<HighLow> special{{}, {-0.0, 0}, {1},
{std::numeric_limits<double>::max()},
{std::numeric_limits<double>::denorm_min()}, {1, 1},
{std::numeric_limits<double>::quiet_NaN(), 0}};
for (std::size_t lane = 0; lane < 4; ++lane)
for (const auto value : special) {
auto changed = input;
changed.deviatoric_left.values[lane] = RawPacketPair(value);
ExpectFrozenMaterialPacket(changed);
fesa::hex8_internal::PairPacket actual;
fesa::hex8_internal::SelectMaterialPacketEvaluator()(changed, &actual);
for (std::size_t other = 0; other < 4; ++other)
if (other != lane) {
EXPECT_EQ(PairBits(actual.values[other].high), PairBits(reference.values[other].high));
EXPECT_EQ(PairBits(actual.values[other].low), PairBits(reference.values[other].low));
}
}
}
TEST(Hex8HighLow, UniformDyadicPacketsExerciseEachLowZeroShape) {
std::size_t checks = 0, avx2_checks = 0;
for (int shape = 0; shape < 4; ++shape)
for (int low_weight = 0; low_weight < 2; ++low_weight)
for (int zero_accumulator = 0; zero_accumulator < 2; ++zero_accumulator) {
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {0.5, low_weight != 0 ? std::ldexp(1.0, -57) : 0.0};
for (std::size_t lane = 0; lane < 4; ++lane) {
const bool left_low = (shape & 1) != 0;
const bool right_low = (shape & 2) != 0;
// Every lane has the same normalized nonzero dyadic inputs. Component
// LBE sums stay above -200 and all Add intermediates are bounded by 4.
// Thus each Multiply low-shape is uniform and its component products
// bypass zero/unsupported-tail fallback. No mixed-zero Add fallback
// is possible; the initial zero accumulator is uniform across lanes.
input.deviatoric_left.values[lane] = {1, left_low ? std::ldexp(1.0, -54) : 0};
input.deviatoric_right.values[lane] = {2, right_low ? std::ldexp(1.0, -54) : 0};
input.shear_left.values[lane] = {0.5, left_low ? std::ldexp(1.0, -56) : 0};
input.shear_right.values[lane] = {0.25, right_low ? std::ldexp(1.0, -57) : 0};
input.tensor_left.values[lane] = {0.75, left_low ? std::ldexp(1.0, -56) : 0};
input.gradient_dot.values[lane] = {0.125, right_low ? std::ldexp(1.0, -58) : 0};
input.accumulated.values[lane] = zero_accumulator != 0
? fesa::hex8_internal::PacketPair{0, 0}
: fesa::hex8_internal::PacketPair{0.0625, 0};
}
avx2_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
++checks;
}
RecordProperty("uniform_low_shape_checks", static_cast<int>(checks));
RecordProperty("avx2_uniform_low_shape_checks", static_cast<int>(avx2_checks));
}
TEST(Hex8HighLow, OverflowLaneIsNeutralizedBeforeResidualFma) {
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
if (evaluator == fesa::hex8_internal::EvaluateMaterialPacketScalar)
GTEST_SKIP() << "AVX2 masked environment is not selected; routing unverified.";
std::fenv_t original;
ASSERT_EQ(std::fegetenv(&original), 0);
for (std::size_t bad = 0; bad < 4; ++bad) {
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {0.5};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {
lane == bad ? std::numeric_limits<double>::max() : 1};
input.deviatoric_right.values[lane] = {2};
input.shear_left.values[lane] = {0.5};
input.shear_right.values[lane] = {0.25};
input.tensor_left.values[lane] = {0.75};
input.gradient_dot.values[lane] = {0.125};
input.accumulated.values[lane] = {0.0625};
}
// All operands are finite/nonzero and max*2 has LBE sum 972, so Product
// reaches its vector multiply; only the post-product finite mask rejects it.
const auto expected = FrozenMaterialPacket(input);
EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0);
fesa::hex8_internal::PairPacket output;
evaluator(input, &output);
const bool extra_invalid = (std::fetestexcept(FE_INVALID) & FE_INVALID) != 0;
EXPECT_EQ(std::fesetenv(&original), 0);
EXPECT_FALSE(extra_invalid);
for (std::size_t lane = 0; lane < 4; ++lane) {
EXPECT_EQ(PairBits(output.values[lane].high), PairBits(expected.values[lane].high));
EXPECT_EQ(PairBits(output.values[lane].low), PairBits(expected.values[lane].low));
}
}
RecordProperty("avx2_product_overflow_positions", 4);
}
TEST(Hex8HighLow, PacketBackendSelectionRecoversAfterEnvironmentRestoration) {
const auto initial = fesa::hex8_internal::SelectMaterialPacketEvaluator();
const int rounding = std::fegetround();
ASSERT_EQ(std::fesetround(FE_UPWARD), 0);
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(),
fesa::hex8_internal::EvaluateMaterialPacketScalar);
EXPECT_EQ(std::fesetround(rounding), 0);
#if defined(_M_X64) || defined(__SSE2__)
const auto control = _mm_getcsr();
_mm_setcsr(control | 0x8040U);
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(),
fesa::hex8_internal::EvaluateMaterialPacketScalar);
_mm_setcsr(control);
_mm_setcsr(control & ~0x80U);
const auto unmasked = fesa::hex8_internal::SelectMaterialPacketEvaluator();
_mm_setcsr(control);
EXPECT_EQ(unmasked, fesa::hex8_internal::EvaluateMaterialPacketScalar);
#endif
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(), initial);
}
TEST(Hex8HighLow, PacketScalarBridgesPreserveOriginalRawBits) {
using fesa::hex8_internal::PacketPair;
using fesa::hex8_internal::PairPacket;
static_assert(std::is_trivial_v<PacketPair> &&
std::is_standard_layout_v<PacketPair>);
static_assert(std::is_trivial_v<PairPacket> &&
std::is_standard_layout_v<PairPacket>);
static_assert(std::is_trivial_v<fesa::hex8_internal::MaterialPacketInput>);
const double tiny = std::numeric_limits<double>::denorm_min();
const std::vector<PacketPair> pairs{{0, 0}, {-0.0, 0}, {0, -0.0},
{1, -0.0}, {1, std::ldexp(1.0, -54)}, {tiny, 0}, {1, tiny},
{std::numeric_limits<double>::max(), 0}, {1, 1},
{std::numeric_limits<double>::infinity(), 0},
{std::numeric_limits<double>::signaling_NaN(), 0}};
for (const auto left : pairs)
for (const auto right : pairs) {
PairPacket a{{left, left, left, left}};
PairPacket b{{right, right, right, right}};
PairPacket result;
const fesa::high_low_original::HighLow x{left.high, left.low};
const fesa::high_low_original::HighLow y{right.high, right.low};
const auto expect = [&](fesa::high_low_original::HighLow expected) {
for (std::size_t lane = 0; lane < 4; ++lane) {
EXPECT_EQ(PairBits(result.values[lane].high), PairBits(expected.high));
EXPECT_EQ(PairBits(result.values[lane].low), PairBits(expected.low));
}
};
fesa::hex8_internal::ScalarPacketAdd(a, b, false, &result);
expect(fesa::high_low_original::Add(x, y));
fesa::hex8_internal::ScalarPacketAdd(a, b, true, &result);
expect(fesa::high_low_original::Add(x, y));
fesa::hex8_internal::ScalarPacketMultiply(a, b, &result);
expect(fesa::high_low_original::Multiply(x, y));
const double high_left[4]{left.high, left.high, left.high, left.high};
const double high_right[4]{right.high, right.high, right.high, right.high};
fesa::hex8_internal::ScalarPacketTwoProduct(high_left, high_right, &result);
expect(fesa::high_low_original::TwoProduct(left.high, right.high));
}
}
TEST(Hex8HighLow, PacketCapabilityRejectsMissingIntegerIsaAndOsState) {
constexpr std::uint32_t leaf1 =
(1U << 12) | (1U << 26) | (1U << 27) | (1U << 28);
constexpr std::uint32_t leaf7 = (1U << 3) | (1U << 5) | (1U << 8);
EXPECT_TRUE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
leaf1, 6, leaf7));
for (const auto missing : {1U << 3, 1U << 5, 1U << 8})
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
leaf1, 6, leaf7 & ~missing));
for (const auto missing : {1U << 12, 1U << 26, 1U << 27, 1U << 28})
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
leaf1 & ~missing, 6, leaf7));
for (std::uint64_t state : {0ULL, 2ULL, 4ULL})
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
leaf1, state, leaf7));
}
TEST(Hex8HighLow, PacketDoesNotClearPriorMaskedStickyFlags) {
std::fenv_t original;
ASSERT_EQ(std::fegetenv(&original), 0);
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
ASSERT_EQ(std::feraiseexcept(FE_INEXACT), 0);
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {1};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)};
input.deviatoric_right.values[lane] = {2, std::ldexp(1.0, -54)};
}
fesa::hex8_internal::PairPacket output;
evaluator(input, &output);
const bool preserved = (std::fetestexcept(FE_INEXACT) & FE_INEXACT) != 0;
EXPECT_EQ(std::fesetenv(&original), 0);
EXPECT_TRUE(preserved);
}
fesa::Hex8Element UnitBrick(double offset = 0, double nu = 0) {
fesa::ElementDofLayout layout;
layout.node_indices = {0, 1, 2, 3, 4, 5, 6, 7};