Repository navigation
fix(element): reuse cached geometry in assembly and drop it when points change - #68
Merged
Merged
Conversation
…ts change
Transformation caches its geometric quantities lazily, but three things
were wrong:
- Assembly bypassed the cache. ElementAssembler and NodeAssembler take
shape gradients from batch_shape_grad_jxw, which used the cache only if
energy() or the shape_grad/JxW properties had filled it, and never filled
it itself. Every call inverted all element Jacobians again.
- update_points did not drop derived buffers, despite its docstring. Once
energy() had filled the cache, asm(points=new) returned the old
geometry's matrix. FacetAssembler and NodeAssembler's compiled path did
so from their second call on.
- With points that require grad, cached geometry kept the first call's
graph. A second backward failed ('backward through the graph a second
time'), and new points were ignored.
A full-range request (batch_size=-1) now fills the shape-gradient and JxW
cache, and later calls reuse it. Chunked requests still compute per chunk
and keep nothing. update_points drops every geometry buffer unless it is
given the tensor it already holds, unmodified. While the points require
grad (with grad mode on), geometry is recomputed on every call.
NodeAssembler's compiled path rebuilds when the geometry version changes.
Jacobian inverses for D <= 3 use a closed-form adjugate (small_inverse)
instead of torch.inverse, which on the CPU makes one LAPACK call per
matrix. A Laplacian on 12k P1 triangles took 148 ms per call on the CPU;
it now takes 2.4 ms on the first call and 0.9 ms after. On 48k tetrahedra
it went from 780 ms to 7.6 ms first and 5.4 ms after. The mixed assembler
and the Nanson scale use small_inverse too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dst8zhPLnxVpZMBhR4Pvfs
Signed-off-by: Shizheng Wen <wenshizheng0710@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Transformationcaches its geometric quantities (shape gradients,JxW, facet Jacobians, ...) lazily as buffers, but the cache had three problems.ElementAssembler.__call__andNodeAssembler.__call__take shape gradients fromTransformation.batch_shape_grad_jxw. That method used the cache only ifenergy()or theshape_grad/JxWproperties had filled it, and never filled it itself. So every plainasm()call inverted every element Jacobian again: one LAPACK call per 2×2/3×3 matrix on the CPU.update_pointsdidn't drop anything, despite its docstring ("reset derived buffers"). Everypoints=argument goes through it.energy()had filled the cache,asm(points=new)returned the old geometry's matrix (max error 1.0 on a stretched mesh).FacetAssembler, whose facet quantities always go through the caching properties, did this from its second call on.NodeAssembler'scompile()fast path, which capturesshape_grad/jxwwhen it is built.forms.rst) promises "all paths re-cache theTransformationon newpoints".backwardfailed with "Trying to backward through the graph a second time".Changes
Transformation):batch_size=-1, theElementAssemblerdefault) fills the shape-gradient +JxWcache, and later calls reuse it.batch_size>0, the memory-saving mode, andNodeAssembler's default) are served from the cache when it exists. Otherwise they compute their chunk and keep nothing, as before.update_pointsdrops every geometry-derived buffer, unless it is handed the tensor it already holds, unmodified (identity +_version). Passing the same coordinates again therefore keeps the cache.jacobianrebuilds them on access, without an inverse. This keeps the cache to what assembly needs.small_inverse(tensormesh/element/element.py, internal): closed-form adjugate inverse for D ≤ 3, falling back totorch.linalg.invabove that. It raisesValueErroron a singular (degenerate) element, astorch.inversedid. It replacestorch.inverseineval_shape_grad, in the Nanson scale and inMixedElementAssembler, which recomputes its geometry on every call by design.NodeAssembler.compile(): the fast path's rebuild key includesTransformation._geometry_version.batch_sizedocstrings say what gets cached.Timings
Laplacian,
quadrature_order=2, one Clariden node (Grace CPU / GH200). "New points" passes a fresh coordinate tensor every call, as a moving mesh would.main: first / repeated / new pointsThe whole test suite on the same node: 278 s on
main, 102 s with this PR.Tests
tests/element/test_transformation_cache.py(17 tests). Meshes are built with NumPy + meshio, so the tests don't need gmsh.asm()evaluates the shape gradients once. Chunked assembly keeps nothing and agrees with full assembly, both before and after the cache is filled.pointsare honoured after the cache was filled, by:ElementAssembler, with the cache filled by assembly or byenergy();NodeAssembler, chunked and full;NodeAssembler.compile();FacetAssembler.Each result is compared with a fresh assembler on the moved mesh.
autograd.gradthrough the same leaf;small_inverseagainsttorch.linalg.invfor D = 1…4,gradcheck, and singular input.Against unmodified
main, 6 of the 10 caching tests fail. Among the failures, the autograd test fails with the graph error above. The other 4 pass, because nothing is cached onmainto go stale.The full suite on a Clariden GPU node gives an identical failure set on
mainand on this branch. All of those failures are environmental: gmsh has no aarch64 build there. The branch adds 17 passes. CI covers the gmsh-dependent tests.Notes
shape_grad([E, Q, B, D]) andJxWperTransformationbetween calls. Its peak memory during a call is unchanged. Memory-bound runs that passbatch_size>0keep nothing, as before.Transformation.jacobianis now computed on its own (no inverse) and cached separately. Previously it was a side product ofshape_grad. The values are identical.🤖 Generated with Claude Code
https://claude.ai/code/session_01Dst8zhPLnxVpZMBhR4Pvfs