perf(shell): route local products through MKL and reduce Hessian storage work

This commit is contained in:
KOKO\Mimi
2026-10-14 18:07:16 +09:00
parent 8c7c41729e
commit f9e0df66dc
9 changed files with 758 additions and 179 deletions
@@ -0,0 +1,162 @@
# Shell 계산 경로의 성능 변경
이 기록은 reduced-coordinate shell의 local 계산 변경과 근거를 구분한다.
전체 시간 목표와 최종 원본 비교는 [PRD](PRD.md)의 계약이며 아래의 단계 검사는
그 계약의 최종 합격을 대신하지 않는다.
## 계측과 변경의 연결
원본 meshfine shell의 기준 전체 실행은 6372.3256209초다. VTune 소프트웨어
샘플링의 별도 150초 부분 수집은 accepted increment 2 / time 0.2까지 포함한다.
`Jet*`는 sampled CPU 133.466초(27.6%), `malloc`은 30.289초(6.3%)였다.
`EvaluateAssembly`의 inclusive 257.862958 CPU초와 중첩 selector의 1.600014초는
겹치는 호출 경로이므로 더하거나 전체 경과 시간으로 바꾸지 않는다.
`nonlinear_mitc4_shell.cpp`의 모든 Jet는 기존에 값이 상수인 경우도 576-double
Hessian을 영 초기화한 `shared_ptr`로 할당했다. 상수의 영 Hessian은 이제
읽기 전용 정적 영 배열을 참조하고 처음 쓰는 때만 기존 공유 storage를 할당한다.
읽기 전용 영 배열에는 trial 값이 없으며 동시 호출은 그 배열을 수정하지 않는다.
이미 할당된 Hessian의 복사/공유 수명은 그대로다. 현재 trial 이외의 형상·접선·
도함수 응답을 저장하거나 주소에 따라 재사용하는 cache는 추가하지 않았다.
후속 후보는 576-entry 반복문 앞에서 입력 read pointer와 fresh output pointer를
한 번씩 취득한다. fresh output은 단일 `make_shared` 할당을 유지하고 영 초기화를
생략한 뒤 576개 전체를 기록한다. `operator+/-/*`, scalar scale, Compose, Atan2의
모든 complete write와 MKL 거부 시의 NaN fill 뒤에만 결과를 읽거나 복사한다.
일반 부분 mutable write는 영 초기화를 유지한다. 상수의 nullptr 복사에는 별도
mutable 공유 객체가 없지만 실제 consumer는 복사된 상수 Hessian을 수정하지 않는다.
이미 할당된 결과의 shared copy와 입력 alias는 수정하지 않는다.
## 일반 행렬 연산의 MKL 경계
| 파일 / 함수 | 연산과 크기 | 변경 |
| --- | --- | --- |
| `mitc4_shell.cpp::Stiffness`, `Congruence` | 5×20 strain의 BᵀCB, 20/4 local→24 congruence | `Matrix::Multiply`와 private transpose-left MKL GEMM, 상삼각 결과를 그대로 대칭 복사 |
| `nonlinear_mitc4_inertia.cpp::CheckPhysicalMass` | 24×20 complement Gram·mass congruence, 20×20 Cholesky·inverse·reconstruction | GEMM 및 기존 private LAPACK helper; 같은 positivity, reconstruction, 2^-32 conditioning gate 유지 |
| `nonlinear_mitc4_inertia.cpp::Evaluate` | 각 lumped mass-node의 6×3 coefficient Gram, velocity/position bilinear, 3방향 mass variation의 6×6 block | 무할당 row-major transpose-right GEMM; 원래 point/node/row/column scatter와 가중 누적 유지 |
| `nonlinear_mitc4_shell.cpp::operator*`, `Compose`, `Atan2`, `DrillingEnergy` | ordinary 24×24 derivative outer products | private `OuterProduct`의 row-major GEMM k=1, 기존 Jet storage에 직접 기록 |
| `hht_dynamic_algebra.cpp::BuildPhysicalSpace` | scaled free mass×analytic null 및 일반 complement congruence의 sparse contributions | entrywise compact-diagonal MKL products 뒤 원래 CSR/COO 순서 누적; full global dense matrix 없음 |
| `hht_dynamic_algebra.cpp::CheckLocalMass` | nonempty nodal physical block, 크기1–6 | MKL Cholesky/inverse; 동일 inverse infinity norm과 2^-32 conditioning 검사 |
| `hht_dynamic_algebra.cpp::CheckRegularity`, `hht_reduced_operators.cpp::CheckGraphRegularity` | scalar null stiffness metric와 gradient×null projection | MKL GEMV/dot 또는 entry products, 기존 fresh factorization/모든 unit RHS/residual/condition gate 유지 |
| `hht_reduced_operators.cpp::ContractConstraintHessian` | failure-only bilinear replay의 sparse Hessian entries | 두 MKL diagonal product 뒤 기존 left association/entry order sum |
Outer product는 배열 copy나 Matrix 객체를 만들지 않는다. 크기·integer range·
nonempty null storage를 검사하며 비유한 입력은 MKL 호출/출력 쓰기 전에 거부한다.
유한 입력의 곱 overflow는 후속 owner finite gate가 판단한다. Jet consumer는
거부된 operand의 결과를 NaN으로 표시하고 원래 equilibrium/constraint finite
실패를 반환한다. 직접 scalar product로 계산하는 fallback은 없다.
`CholeskyLowerAndInverse`의 optional private failure reason은 validation 전에
`kNone`으로 초기화된다. factor pivot/finite 실패와 inverse/finite 실패를 구분하여
기존 positive-mass와 ill-conditioned-mass 진단을 유지한다. 실패에서는 lower와
inverse output candidate를 교체하지 않는다. 기존 default-nullptr consumer의
bool 의미는 그대로이며 live inverse 실패를 발생시킨 검사는 아직 없다.
Element-local dense work의 `ScopedLocalDenseThreads`는 caller TLS 요청이 default
0일 때만 1을 적용한다. caller의 양수 override는 유지하고 정상 반환과 unwind 뒤
원래 default를 복원한다. 전역 MKL/OpenMP 정책이나 이후 global solve에는 영향을
주지 않는다. 각 scalar metric scope는 MKL 호출 뒤 끝나며 global PARDISO를 포함하지
않는다. 현재 후보의 실제 비용과 복원 검사는 아래 단계 검증 기록으로 구분한다.
TLS default 0, caller override 우선순위와 저장한 값을 복원하는 동작은
[Intel 2026 thread-local reference](https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2026-0/mkl-set-num-threads-local.html)의 계약을 따른다.
Hessian의 합산 순서와 ordinary product는 별도다. 곱셈 Jet는 원래의
`left.second * right.value + outer1 + outer2 + left.value * right.second`를
유지한다. Compose와 Atan2는 계수×gradient를 먼저 계산하여 원래 left association을
유지한 뒤 outer product를 계산한다. GEMM의 연산과 signed-zero 결과는 원래 scalar
구현의 bit 동일성을 주장하지 않으며 접선/방향/혼합 도함수의 수치 검토 대상이다.
일반 24×24 연산을 AD라는 이유로 특수 정밀도 예외로 처리하지 않는다.
## 유지하는 특수 연산
| 파일 / 함수 | 크기·정밀도·이유 | 검증 범위 |
| --- | --- | --- |
| `nonlinear_mitc4_inertia.cpp::Jet`, `ExpSkew`, `LeftJacobian` | 3개의 nodal rotation coordinate에 대한 최대 3차 analytic derivative; 3×3×3 tensor | finite rotation, inertia force/position/velocity tangent와 null·momentum oracle |
| `hht_reduced_operators.cpp::BasisValue`, fixed basis 생성 | normalized high+low 및 outward radius; 3-vector basis certificate | fixed chart/radius replay·partial-boundary·selector transversality |
| `hht_reduced_operators.cpp::RateSum`, `GradientDot`, reduced block 누적 | sparse support의 원래 순서 Neumaier correction | cancellation, full defect, mixed rates·channel acceptance |
| `nonlinear_mitc4_inertia.cpp::InertiaSum` | kinetic force/energy/momentum의 원래 순서 compensated contraction | 독립 inertia oracle와 omission 검출, 좌표/단위 변화 |
| shell의 fixed3 coordinate tensor/rotation 변환 | 좌표·director·chart의 3-vector 또는 3×3 연산 | rigid motion/objectivity·principal chart 실패 |
| `hht_dynamic_algebra.cpp::BuildPhysicalSpace`의 analytic nodal basis Gram, `InitializeAcceleration`의 force projection/lift | translation 1계수 또는 단일 절점의 최대3 rotation components; 현재 assembly의 nodal frame에 대한 Float64 좌표 변환 | 서로 다른 node의 basis support는 disjoint, 같은 node의 최대3-vector inner product만 사용; excluded fixed rows·원래 association·초기 acceleration 진단 순서 유지 |
보상 누적과 interval/basis 인증은 일반 Float64 GEMM으로 접어 수치 의미를 잃지 않는다.
`hht_reduced_operators.cpp::GradientDot`, `Lift`, `ProjectPhysical`,
`ReconstructPhysicalRates`, `AssembleAugmentedReducedSystem`의 support contractions는
원래 entry 순서의 Neumaier correction과 high+low/outward fixed-basis certificate를
유지하는 예외다. Plain sum과 달리 partition cancellation에서 소실되는 1을 유지하는
`ReconstructsRatesAcrossSelectedPartitionCancellation` 및 augmented full-model defect
검사가 해당 의미를 검증한다. 일반 24×24 outer와 mass matrix 제품은 이 예외로 묶지 않는다.
`BuildPhysicalSpace`의 좌표 basis는 현재 assembly의 analytic null로 각 호출에서
생성한다. 초기화는 initial assembly를 사용하고 후속 질량 인증은 해당 현재 assembly를
사용한다. 실행 전체의 fixed initial chart와 같은 자료라고 판단하지 않으며
다른 trial에 basis/응답을 재사용하지 않는다.
## 단계 검증 상태
변경 전 clean source `8c7c41729eb9eaa904f6739b54d34c37b1bd555f`의 fresh MSVC
Debug configure/build는 native 0, local shell/inertia/reduced/math 70개는 모두 PASS다.
별도 MITC4 linear kernel·mass·recovery 20개도 native 0으로 모두 PASS다.
Mixed-time actual shell fixture는 144.84초에 정상 통과했으며 중단되지 않았다.
[원래 실행 기록](../../.harness/performance/t4/baseline-native.json)과
[linear 실행 기록](../../.harness/performance/t4/baseline-linear-native.json)을 보존한다.
첫 후보 build의 `Status` 기본 생성자 오류(native 2)는 별도로 보존하고
`Status::Ok` 초기화로 수정했다. 수정 후보의 configure/build는 native 0이고
91개 중 90 PASS, `HhtReducedEmbedding.ReconstructsActualShellRatesWithMixedTimeLoad`는
180초 timeout으로 미통과다. 전체 CTest native 8과 실제 종료 상태를 보존한다.
이 Debug 비용 악화를 Release 개선으로 표시하지 않는다. 이는
`candidate-local-fixed` snapshot의 결과다. Read pointer hoist, fresh output의
단일 allocation/full overwrite, 별도 이름의 local TLS scope와 추가 coverage를
적용한 `packed-output-mkl-coverage` 결과는 별도로 기록한다.
기존 Korean compiler 환경의 Ninja header prefix가 mojibake이고 actual dependency
graph가 0개인 사실을 확인했다. 그 build의 직접 변경 source/검사 결과는 보존하되
header consumer 전체 재빌드를 입증한다고 해석하지 않는다. 설치된 compiler의
locale fallback 때문에 `VSLANG=1033`만으로 해결되지 않는다는 root의 후속 근거가
있다. 정확한 include prefix를 지정한 ignored metadata helper의 별도 probe 뒤
full fresh build에서 actual Ninja header deps를 확인했다. 공용 preset/flag나
source object를 바꾸어 혼합하지 않는다.
후속 `packed-output-mkl-coverage` snapshot의 source SHA-256은
[source manifest](../../.harness/performance/t4/candidate-packed-source.json)에 있다.
Corrected-prefix fresh configure/build는 각각 native 0이고
`eigen_reduction.cpp.obj`의 실제 dependency inventory에
`dense_blas_internal.h`가 포함되어 있다.
기존 Cholesky/eigen consumer, local tangent/objectivity/patch·inertia,
fixed chart·channel/failure·boundary·MKL scope/shape/finite 검사 **105/105 PASS**,
native 0, CTest 36.94초다. 별도 mixed-time fixture도 **1/1 PASS**, native 0,
fixture 60.69초다. [실행 기록](../../.harness/performance/t4/packed-native.json),
[검사 로그](../../.harness/performance/t4/packed-4.txt),
[mixed-time 기록](../../.harness/performance/t4/packed-mixed-native.json)을 보존한다.
이 106개 결과는 전체 Debug/원본 비교/시간수렴을 통과했다는 뜻이 아니다.
첫 Release 보조 configure는 runtime DLL PATH를 compiler 탐색보다 앞에 두어
icx가 선택되었고 `libircmt.lib` 없음으로 native 1 실패했다. 수치 실행은 없었으며
실패 자료/cache를 보존한다. 동일 설치 MSVC cl을 명시한 별도 fresh directory의
Release configure/build 97 edges는 native 0이다. [보조 실행 기록](../../.harness/performance/t4/release-micro-fixed-native.json)을 보존한다.
Jet 보조 측정은 frozen `8c7c417`의 실제 Jet source span과 현재 production
`nonlinear_mitc4_shell.cpp`를 같은 translation unit에 두고 현재 source의
Release library와 연결했다. 동일 입력·flags로 각 variant의 3000-product loop를
워밍업 없이 한 번씩 측정했다. 모든 value/gradient/Hessian/directional/mixed
coefficient 및 checksum이 같고 실제 native 0, scope 안 effective threads 1,
종료 뒤 caller TLS 복원 true다. 정확성 대조는 모든 타이밍 후 수행했다.
| 보조 variant | 3000-product loop 경과 시간 |
| --- | ---: |
| frozen original | 0.0025384초 |
| MKL candidate / default 8 | 0.0082295초 |
| MKL candidate / element scope 1 | 0.0029779초 |
Scope는 새 MKL kernel의 default-thread 비용을 줄이지만 이 dense nonzero
kernel 자체가 원래 구현보다 빠르다는 근거는 아니다. Scope 후보도 original보다
약17% 느리다. 전체 모델의 준비·조립·allocator·선형 해법·출력 비용과 최종
100초 목표는 이 보조 값으로 판단하지 않는다. 실제 production의 Constant/Jet
allocation 빈도 감소는 최종 VTune에서 확인해야 한다.
Standalone include의 vendor C4819 경고는 보존했다. 실제 production과 같은
`/W4 /WX /external:W0`로 compile/link만 추가 실행하여 native 0/경고 없음이고
수치 타이밍은 반복하지 않았다. 측정된 exe는 별도 원본 copy로 보존했으며
추가 exe의 `.text` SHA-256이 동일한 것을 확인했다.
[기계어/실행 파일 보존 기록](../../.harness/performance/t4/micro-warning-policy-text-comparison.json)을 따른다.
전체 Debug·원본 비교·C08 current grid·VTune 최종 수집·공식 단일 시간 측정은
이 단계에서 실행하지 않았다. 변경되지 않은 reference와 tolerance를 사용해야 한다.