fix(hht): preserve metric association and complete MKL actions

This commit is contained in:
KOKO\Mimi
2026-10-14 18:38:00 +09:00
parent 2853051896
commit c58d499727
6 changed files with 155 additions and 43 deletions
@@ -160,3 +160,23 @@ Standalone include의 vendor C4819 경고는 보존했다. 실제 production과
[기계어/실행 파일 보존 기록](../../.harness/performance/t4/micro-warning-policy-text-comparison.json)을 따른다.
전체 Debug·원본 비교·C08 current grid·VTune 최종 수집·공식 단일 시간 측정은
이 단계에서 실행하지 않았다. 변경되지 않은 reference와 tolerance를 사용해야 한다.
## f9 고정 source 후속 SW 프로파일 및 metric/coverage 수정
f9e0df66dc9ab04e6a3e295e7ade1df6add15193 source의 Release CLI target 빌드 native0,2.210초. 기존 동일 source의 fresh MSVC Release LIB 디렉터리에 정식 fesa_cli target만 --parallel4 빌드했으며 다른 source object를 복사하지 않았다. parent `.harness/performance/t4/cli-build-native.json`, `cli-runtime-hashes.json` 및 `f9-shell-sw-150s/frozen-profile-source.json`에 실제 argv/settings/source/CLI/DLL/PDB hash를 고정했다.
동일 원본 input SHA B8922243CA8DDFE20B1DD39F3123207F789A1A75F6B10DBB6B0BC0B5A40DFFF5를 scratch에 복사해 SW hotspots150초를 1회 수집했다. Collector native4는 제한 수집의 partial 종료이며 target native exit는 미확인이다. Collector 총171.409초, watchdog240초 미발동, ownedtarget18316 종료 및 remaining_targets=[] 확인. 원본/scratch 입력 hash 유지. summary/functions/callstacks export는 각각 native0이다. 수집 자료는 parent `.harness/performance/t4/f9-shell-sw-150s/`에 보존한다.
VTune summary elapsed149.866초/CPU466.466초. Self CPU: shell Jet*85.260초, iomp64.371초, MKL dgemm40.878초, PARDISO kernel31.029초, malloc24.373초, shell Jet+23.937초, inertia Jet*23.821초, errchk_dgemm12.305초, MultiplyTransposeRight8.105초. Callstack inclusive attribution은 EvaluateAssembly240.854초, EvaluateConstraintOperators78.270초, EvaluateInternalResidualAndTangent254.027초, MultiplyTransposeRight86.911초, CheckGraphRegularity2.386초다. Inclusive 값은 서로 겹치며 합산하거나 벽시계로 사용하지 않는다. 작은 MKL dispatch 비용과 내부 constraint 재평가 비용은 후속 후보의 근거다. Sample에는 constant operand/proven-zero derivative 여부가 없으므로 영도함수 생략의 실제 비중은 미확인이다.
150초 구간 마지막 accepted increment1/time0.1/iteration3이다. 기존150초 baseline은 increment2/time0.2였으므로 현재 후보를 전체 성능 개선이나100초 합격으로 기록하지 않는다. f9 source의 증거는 이후 수정에 소급하지 않는다. Callstack parser 최초 PowerShell 시도는 blank separator row에서 numeric parse 오류로 중단했고, 명시적 blank delimiter를 처리하는 `attribute-callpaths.py` native0 결과를 사용한다.
프로파일을 고정한 뒤 root85ae2eb를 병합한 source2853051896091ca0e612c95a65a442f48a8e5558에서 후속 수정했다. 공통 high+low finite classifier와 생성 build ignore를 동기화했으며 공통 public API/flags 변경은 없다. 해당 공통 수정의 독립 검증은 root evidence 소유다.
추가 독립 검토는 metric GEMV의 overflow 반례를 발견했다. M=DBL_MAX, epsilon=2^-26, t=sqrt(1-epsilon^2), n=(epsilon,0.6t,0.8t), B=[[0,M,M],[M,0,0],[M,0,0]]에서 기존 ordered (n_i*B_ij)*n_j 합은 finite positive지만 B*n의 첫 성분은 overflow한다. `HhtReducedOperators.NullMetricKeepsFiniteLeftAssociatedProducts`를 먼저 추가해 실제 RED(native8/1failed), 이후 GREEN(native0/1PASS)를 확인했다. PSD 가정을 새로 추가하거나 accepted metric 범위를 좁히지 않는다.
두 metric caller(CheckGraphRegularity/CheckRegularity)는 analysis-private `OrderedNullMetric`으로9entry MKL diagonal product를 두 번 계산하고 기존 i/j 순서로 누적한다. 첫 product가 nonfinite이면 원래 두 번째 product도 nonfinite이며 최종 양의 finite gate를 통과할 수 없으므로 caller의 기존 진단 위치로 NaN을 반환한다. Source nonfinite도 같은 caller gate로 반환한다. 실제 원래 assertion/failure category/condition2^-32/scale/diagnostic order를 유지한다. 불필요해진 SparseEntry만 제거했다.
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 검토를 추가로 받는다.
+29 -14
View File
@@ -495,20 +495,8 @@ Result<Regularity> CheckRegularity(const Domain& domain,
for (std::size_t i = 0; i < nodes.size(); ++i) {
node_null[nodes[i]] = i;
const auto& n = assembly.rotations[nodes[i]].drill_null;
Matrix metric{3U, 3U};
Vector direction{3U};
for (std::size_t a = 0; a < 3; ++a) {
direction[a] = n[a];
for (std::size_t b = 0; b < 3; ++b) {
metric(a, b) = Entry(assembly.drilling_metric, 6U * nodes[i] + 3U + a,
6U * nodes[i] + 3U + b);
}
}
double stiffness = 0.0;
{
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
stiffness = direction.Dot(metric.Multiply(direction));
}
const double stiffness = OrderedNullMetric(
assembly.drilling_metric, nodes[i], n);
if (!std::isfinite(stiffness) || !(stiffness > 0.0)) {
return Result<Regularity>::Failure(
Failure(domain, "dynamic-algebraic-regularity",
@@ -674,6 +662,33 @@ Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly,
} // namespace
double OrderedNullMetric(const SparseMatrix& metric, const std::size_t node,
const Vector3& direction) {
Vector left{9U};
Vector entries{9U};
Vector right{9U};
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j) {
const auto index = 3U * i + j;
left[index] = direction[i];
entries[index] = Entry(metric, 6U * node + 3U + i,
6U * node + 3U + j);
right[index] = direction[j];
if (!std::isfinite(left[index]) || !std::isfinite(entries[index]) ||
!std::isfinite(right[index]))
return std::numeric_limits<double>::quiet_NaN();
}
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
const Vector first = dense_blas_internal::MultiplyDiagonal(left, entries);
for (std::size_t i = 0U; i < first.Size(); ++i)
if (!std::isfinite(first[i]))
return std::numeric_limits<double>::quiet_NaN();
const Vector products = dense_blas_internal::MultiplyDiagonal(first, right);
double value = 0.0;
for (std::size_t i = 0U; i < products.Size(); ++i) value += products[i];
return value;
}
std::vector<std::size_t> NullNodes(
const DofManager& dofs,
const std::vector<DynamicRotationKinematics>& rotations) {
+45 -3
View File
@@ -7,6 +7,7 @@
#include <utility>
#include "analysis/hht_dynamic_internal.h"
#include "math/dense_blas_internal.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/finite_rotation.h"
#include "fesa/loads/load.h"
@@ -54,6 +55,27 @@ Vector Gather(const Vector& full, const RuntimeShell& shell) {
return local;
}
/// @brief Preserves rejected entries while batching finite products in MKL.
Vector EntryProducts(const Vector& left, const Vector& right) {
Vector result(left.Size(), std::numeric_limits<double>::quiet_NaN());
std::vector<std::size_t> positions;
positions.reserve(left.Size());
for (std::size_t i = 0U; i < left.Size(); ++i)
if (std::isfinite(left[i]) && std::isfinite(right[i])) positions.push_back(i);
Vector valid_left{positions.size()};
Vector valid_right{positions.size()};
for (std::size_t i = 0U; i < positions.size(); ++i) {
valid_left[i] = left[positions[i]];
valid_right[i] = right[positions[i]];
}
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
const Vector products =
dense_blas_internal::MultiplyDiagonal(valid_left, valid_right);
for (std::size_t i = 0U; i < positions.size(); ++i)
result[positions[i]] = products[i];
return result;
}
} // namespace
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
@@ -455,6 +477,15 @@ Result<DynamicAssembly> EvaluateAssembly(
}
}
const auto a = Gather(frame.acceleration, shells[e]);
Vector mass_values{24U * 24U};
Vector acceleration_values{24U * 24U};
for (std::size_t row = 0U; row < 24U; ++row)
for (std::size_t column = 0U; column < 24U; ++column) {
const auto index = 24U * row + column;
mass_values[index] = m.mass(row, column);
acceleration_values[index] = a[column];
}
const Vector mass_action = EntryProducts(mass_values, acceleration_values);
for (std::size_t row = 0U; row < 24U; ++row) {
const auto global = shells[e].scatter[row];
frame.internal_force[global] += f.internal_force[row];
@@ -466,7 +497,7 @@ Result<DynamicAssembly> EvaluateAssembly(
std::abs(f.drilling_force[row]);
for (std::size_t column = 0U; column < 24U; ++column) {
frame.residual_scale[global] +=
std::abs(m.mass(row, column) * a[column]);
std::abs(mass_action[24U * row + column]);
const std::array<double, 6> values{
m.mass(row, column), m.position_tangent(row, column),
m.velocity_tangent(row, column), f.internal_tangent(row, column),
@@ -543,9 +574,20 @@ Result<DynamicAssembly> EvaluateAssembly(
Vector hidden_second(domain.Nodes().size());
Vector hidden_second_scale(domain.Nodes().size());
for (std::size_t node = 0U; node < constraint_hessians.size(); ++node) {
Vector coefficients{constraint_hessians[node].size()};
Vector first_velocity{coefficients.Size()};
Vector second_velocity{coefficients.Size()};
std::size_t index = 0U;
for (const auto& entry : constraint_hessians[node]) {
const double term = entry.second * frame.velocity[entry.first.first] *
frame.velocity[entry.first.second];
coefficients[index] = entry.second;
first_velocity[index] = frame.velocity[entry.first.first];
second_velocity[index] = frame.velocity[entry.first.second];
++index;
}
const Vector products = EntryProducts(
EntryProducts(coefficients, first_velocity), second_velocity);
for (std::size_t i = 0U; i < products.Size(); ++i) {
const double term = products[i];
hidden_second[node] += term;
hidden_second_scale[node] += std::abs(term);
}
+5
View File
@@ -95,6 +95,11 @@ std::vector<std::size_t> NullNodes(
const DofManager& dofs,
const std::vector<DynamicRotationKinematics>& rotations);
/// @brief Contracts a nodal metric with original left-associated products.
/// @note Nonfinite products remain for the caller's existing validity gate.
double OrderedNullMetric(const SparseMatrix& metric, std::size_t node,
const Vector3& direction);
/// @brief Solves the positive-mass and differentiated-null initial equations.
///
/// The returned candidate is finite and exact-zero at constrained DOFs. Its
+2 -26
View File
@@ -87,19 +87,6 @@ Result<SparseMatrix> DenseSparse(const Matrix& matrix) {
std::move(entries));
}
double SparseEntry(const SparseMatrix& matrix, const std::size_t row,
const std::size_t column) {
const auto first = matrix.RowOffsets()[row];
const auto last = matrix.RowOffsets()[row + 1U];
const auto found = std::lower_bound(matrix.ColumnIndices().begin() + first,
matrix.ColumnIndices().begin() + last,
column);
if (found == matrix.ColumnIndices().begin() + last || *found != column)
return 0.0;
return matrix.Values()[static_cast<std::size_t>(
found - matrix.ColumnIndices().begin())];
}
bool ValidBasis(const FixedChart& chart) {
if (chart.p_columns.size() != chart.p_dofs.size() ||
chart.z_columns.size() != chart.z_dofs.size()) return false;
@@ -1211,19 +1198,8 @@ Result<double> CheckGraphRegularity(
Failure("Reduced graph regularity node is invalid.",
"dynamic-mass-regularity"));
const auto& null = assembly.rotations[node].drill_null;
Matrix metric_block{3U, 3U};
Vector direction{3U};
for (std::size_t i = 0U; i < 3U; ++i) {
direction[i] = null[i];
for (std::size_t j = 0U; j < 3U; ++j)
metric_block(i, j) = SparseEntry(
assembly.drilling_metric, 6U * node + 3U + i, 6U * node + 3U + j);
}
double metric = 0.0;
{
const dense_blas_internal::ScopedLocalDenseThreads local_threads{};
metric = direction.Dot(metric_block.Multiply(direction));
}
const double metric = hht_internal::OrderedNullMetric(
assembly.drilling_metric, node, null);
if (!std::isfinite(metric) || !(metric > 0.0))
return Result<double>::Failure(Failure(
"Reduced graph requires a positive finite null drilling metric.",
@@ -1141,4 +1141,58 @@ TEST(HhtReducedChannelNewton, RestrictedSingularityIsNotFullInvertibility) {
EXPECT_FALSE(solver.FactorizeFresh(restricted.Value().tangent).IsOk());
}
TEST(HhtReducedOperators, NullMetricKeepsFiniteLeftAssociatedProducts) {
const auto fixture = MakeFixture(false);
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain,
fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto assembled = Assemble(fixture, shells.Value(), zero, zero, zero,
zero, false);
ASSERT_TRUE(assembled.HasValue());
auto assembly = assembled.Value();
const double maximum = (std::numeric_limits<double>::max)();
const double epsilon = std::ldexp(1.0, -26);
const double t = std::sqrt(1.0 - epsilon * epsilon);
const std::array<double, 3U> n{epsilon, 0.6 * t, 0.8 * t};
const std::array<double, 9U> block{0.0, maximum, maximum,
maximum, 0.0, 0.0,
maximum, 0.0, 0.0};
double original_metric = 0.0;
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j)
original_metric += (n[i] * block[3U * i + j]) * n[j];
ASSERT_TRUE(std::isfinite(original_metric));
ASSERT_GT(original_metric, 0.0);
ASSERT_FALSE(std::isfinite(block[1U] * n[1U] + block[2U] * n[2U]));
std::vector<fesa::CooContribution> entries;
for (std::size_t node = 0U; node < assembly.rotations.size(); ++node) {
assembly.rotations[node].drill_null = fesa::Vector3(n);
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j)
entries.push_back({6U * node + 3U + i, 6U * node + 3U + j,
block[3U * i + j], node, 3U * i + j});
}
const auto metric = fesa::hht_internal::MakeSparse(
fixture.dofs.FullDofCount(), fixture.dofs.FullDofCount(),
std::move(entries));
ASSERT_TRUE(metric.HasValue());
assembly.drilling_metric = metric.Value();
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(
fixture.dofs, assembly.rotations);
ASSERT_TRUE(chart.HasValue());
fesa::hht_reduced_internal::ConstraintOperators operators;
operators.rows.resize(chart.Value().null_nodes.size());
for (std::size_t row = 0U; row < operators.rows.size(); ++row)
for (std::size_t component = 0U; component < 3U; ++component)
operators.rows[row].gradient.push_back(
{6U * chart.Value().null_nodes[row] + 3U + component,
original_metric * n[component]});
fesa::MklPardisoTangentSolver solver;
const auto checked = fesa::hht_reduced_internal::CheckGraphRegularity(
assembly, chart.Value(), operators, solver);
ASSERT_TRUE(checked.HasValue());
EXPECT_GE(checked.Value(), std::ldexp(1.0, -32));
}
} // namespace