0.2.6 G12 Lot 2 — performance-wall diagnosis¶
This lot diagnoses the linear_static performance wall before any solver
optimization. It changes only verification infrastructure and evidence; no
file under src/solveur was modified. The controlled baseline recorded by
the runner is 4dc8af83d8d45d6a4d61f242aa6b1f974d87bdb3.
Machine-readable artifacts:
- contract:
qualification/0_2_6/g12_lot2_contract.json - scaling:
qualification/0_2_6/g12_lot2_scaling.json - profiles:
qualification/0_2_6/g12_lot2_profiles.json - same-domain high-order comparison:
qualification/0_2_6/g12_lot2_high_order_fair.json - aggregate diagnosis:
qualification/0_2_6/g12_lot2_diagnostic.json - runner:
scripts/benchmark_g12_lot2.py
Harness audit¶
The Lot-1 nnz field was found to represent the reduced matrix resource
estimate rather than the final global stiffness matrix. The Lot-1 harness
and contract now report global_stiffness_nnz/nnz from the assembler's
final_nnz before constraint reduction, and report
reduced_stiffness_nnz separately. The corrected Lot-1 replay preserved the
solution checksums and residuals.
Lot-2 timing is isolated from intrusive memory instrumentation: child timing
runs do not enable the RSS sampler or tracemalloc; a parent process samples
RSS separately. The previous Lot-1 approximately-10k timeout occurred with
both instruments active in the timed process and is classified as
HARNESS_ERROR, not as a solver resource boundary. The clean Lot-2 retest
completed 10,125 actual DOF in 24.332 s.
The route reports solution checksum, residual and finite metrics for every
measured run. No checksum/status/residual drift was observed, so
NUMERICAL_REGRESSION_DETECTED is NO.
Controlled scaling¶
Route: linear_static, family: connected structured TET4, one measured run
per size, timeout 120 s. The requested intermediate sizes all completed.
| target DOF | actual DOF | global nnz | reduced nnz | assembly (s) | sparse conversion (s) | BC (s) | solve (s) | total wall (s) | peak RSS (MiB) | residual |
|---|---|---|---|---|---|---|---|---|---|---|
| 3,000 | 3,000 | 95,172 | 85,252 | 2.671 | 0.013 | 0.00013 | 0.020 | 4.146 | 90.0 | 8.59e-12 |
| 5,000 | 5,184 | 168,924 | 154,360 | 6.558 | 0.027 | 0.00019 | 0.038 | 9.154 | 100.3 | 7.13e-12 |
| 7,500 | 8,232 | 273,348 | 253,252 | 13.804 | 0.041 | 0.00025 | 0.069 | 18.124 | 114.4 | 6.07e-12 |
| 10,000 | 10,125 | 338,727 | 315,532 | 19.296 | 0.045 | 0.00028 | 0.087 | 24.332 | 124.7 | 6.02e-12 |
The ordinary log-log slopes over actual DOF are approximately 1.625 for
assembly, 1.685 for load assembly, 1.665 for load_balance, 1.018 for sparse
conversion, 1.199 for the linear solve, and 1.458 for total wall time. These
are measurements over four sizes, not extrapolation or a product guarantee.
Profile diagnosis¶
The cProfile runs used the same route but disabled RSS/tracemalloc in the timed child. The largest profile completed at 10,125 actual DOF in 51.106 s under cProfile; profile absolute times are not compared directly to the scaling timings because profiling itself adds overhead. Its ten most expensive entries by cumulative time were:
| rank | function | calls | cumulative (s) | self (s) | cumulative share |
|---|---|---|---|---|---|
| 1 | _run_once (diagnostic wrapper) |
1 | 51.098 | ~0 | 100.00% |
| 2 | _run_with_load_balance_timer (diagnostic wrapper) |
1 | 50.957 | ~0 | 99.72% |
| 3 | _run_with_constraint_timer (diagnostic wrapper) |
1 | 50.957 | 0.006 | 99.72% |
| 4 | LinearStaticSolver.solve |
1 | 50.951 | ~0 | 99.71% |
| 5 | runner timed load-balance wrapper |
230 | 39.427 | 0.002 | 77.16% |
| 6 | solveur.loads.integration.load_balance |
230 | 39.425 | 4.559 | 77.15% |
| 7 | GlobalAssembler.assemble_loads |
1 | 38.774 | ~0 | 75.88% |
| 8 | GlobalAssembler._assemble_load_data |
1 | 38.774 | 0.007 | 75.88% |
| 9 | numpy.core.numeric.cross |
842,106 | 28.089 | 11.328 | 54.97% |
| 10 | MeshValidator.validate |
1 | 8.461 | 0.001 | 16.56% |
The remaining profile entries identify the same mesh-quality path:
_element_quality_details (7.716 s), MeshQuality.tet_metrics (7.479 s),
numpy.moveaxis (14.423 s cumulative), DOF has/normalize_dof_name,
_tet_radius_ratio, and triangle-area helpers. The phase timers and the
profile agree: load_balance is the primary component, mesh-quality
validation is secondary, and the CG solve is not the wall.
Memory and storage¶
Global CSR storage grew from 1,154,068 bytes at 3,000 DOF to 4,105,228 bytes at 10,125 DOF, with a log-log slope of approximately 1.043. Global nnz/DOF grew from 31.724 to 33.455 and matrix storage/DOF from 384.7 to 405.5 bytes. Parent-sampled RSS was 90.0, 100.3, 114.4 and 124.7 MiB respectively. The RSS slope is not interpreted as allocation complexity because it includes the child process baseline and warm-up. No independent public-route counter exposes all copies/conversions; sparse conversion and unattributed solver time are recorded, and no unmeasured copy is claimed as a bottleneck.
High-order same-domain check¶
The fair comparison uses one unit tetrahedron (TET4/TET10) or one unit
cube (HEX8/HEX20) with the same material and boundary convention. It is a
resource/implementation comparison, not equal-accuracy evidence.
| family | DOF | global nnz | CSR bytes | nnz/DOF | CSR bytes/DOF | assembly (s) | solve (s) |
|---|---|---|---|---|---|---|---|
| TET4 | 12 | 72 | 916 | 6.0 | 76.3 | 0.00082 | 0.00132 |
| TET10 | 30 | 690 | 8,404 | 23.0 | 280.1 | 0.00256 | 0.00122 |
| HEX8 | 24 | 576 | 7,012 | 24.0 | 292.2 | 0.00365 | 0.00129 |
| HEX20 | 60 | 3,600 | 43,444 | 60.0 | 724.1 | 0.03268 | 0.00131 |
The measured ratios are TET10/TET4: 3.114x assembly, 9.583x global nnz and 9.175x CSR storage; HEX20/HEX8: 8.947x assembly, 6.25x global nnz and 6.196x CSR storage. The one-element runs are too small for a performance claim beyond this controlled comparison.
Diagnosis and next-step candidates¶
PRIMARY_BOTTLENECK = PYTHON_ASSEMBLY, specifically repeated
load_balance work inside load assembly. At the largest profiled case it
consumes 39.427 s (77.15% of profiled wall), while mesh validation consumes
8.461 s (16.56%). Sparse conversion (0.055 s) and CG solve (0.096 s) are not
primary bottlenecks. The clean intermediate scaling confirms completion
through 10k target DOF; no solver timeout remains in this controlled range.
Candidate work is deliberately deferred to a future optimization lot:
- High priority: batch/vectorize
load_balanceand avoid a full node scan for each nodal load; the current call/visit counts and profile support this as the largest opportunity. - Medium-high: cache immutable mesh-quality validation only if the product and verification contract permits it.
- Medium: reduce repeated DOF-name normalization/index lookup in hot loops.
- Low: investigate sparse assembly internals after the two Python paths; current measurements do not justify prioritizing the CG solver.
No candidate is implemented in Lot 2. No functional solver code changed.