Repository navigation
Fuse dense elliptic curvature into the Hessian tile - #1879
adenzler-nvidia wants to merge 11 commits into
Conversation
Dense Newton built the elliptic cone curvature in a separate contact-major kernel that strided over every contact in the batch for every upper-triangle Hessian entry and accumulated into the Hessian with atomics. Every solver pass paid that full scan, including passes in which almost every world had already converged, and each contact's rows were reloaded once per Hessian entry. The cone curvature of a contact is diagonal plus rank one, so it can be written as a sum of squared, weighted combinations of the contact's own Jacobian rows: the normal direction minus the projected tangent, plus an orthonormal (Householder) basis of the tangent space orthogonal to the sliding direction. Each cone-state row maps to one such virtual row and weight, and the existing per-world J'WJ tile product of the dense JTDAJ kernel then assembles the full Hessian. This mirrors the sparse path, which already folds elliptic curvature into its per-group assembly. The constraint update already loads the contact, friction and tangent residuals when it classifies a row into the cone state, so it now also writes the row's virtual-row coefficients, weight and the contact's first row into solver-context arrays that exist only for dense elliptic Newton. The Hessian kernel tile-loads the Jacobian chunk, takes the cone weights from the context, and rewrites only the cone-state rows of chunks that contain any. The dense Hessian is now built by one per-world kernel without atomics, converged worlds exit at the start of the block, and the standalone dense JTCJ kernel and its launch sizing are removed. Cone curvature is selected by constraint state alone, as in the sparse path and MuJoCo. The dense Hessian test now compares the curvature added by the elliptic kernel against the independent NumPy reference for mixed 3/4/6 contacts across two heterogeneous worlds, and the compact solver equivalence test also covers dense Jacobians.
a9730f0 to
32332f3
Compare
…ptic-fused # Conflicts: # mujoco_warp/_src/types.py
|
Thanks @adenzler-nvidia! Fusing the dense elliptic cone curvature into the per-world 1.
|
|
@yuvaltassa investigating porting some of these optimizations to mujoco c |
…ptic-fused # Conflicts: # mujoco_warp/_src/determinism_test.py
The constraint update returned early for rows of an elliptic contact that an njmax or naconmax overflow had truncated, without writing their force or state. Both persist across steps, so such a row kept whatever an earlier step had left there. With dense elliptic Newton a stale cone state made the Hessian kernel read cone coefficients and weights that were never written for that row. These rows now get zero force and the satisfied state. The cone weights are tile-loaded in whole row chunks like efc.D, so they are now allocated with the padded row count; with njmax not a multiple of the chunk size the last chunk read past the allocation. Both dense Hessian kernels clamp nefc to njmax, and chunks are only scanned for cone rows below nefc. The tangent residuals of a cone row are kept from the loop that already loads them instead of being read again. The dense JTCJ kernel was the only reader of the upper-triangle index arrays dof_tri_row and dof_tri_col on Model and cdof_tri_row and cdof_tri_col on Data, so they are removed. The compact equivalence test now slides the spheres so that elliptic contacts are in the cone state and, for elliptic cones, takes a single Newton step from the warmstart, where qacc depends on the cone Hessian of the compact kernel.
The two dense Hessian kernels shared a wp.func that returned the chunk's weight tile. Warp allocates a tile returned by a function in shared memory but does not count it in the shared memory the kernel requests, so the kernels overran their shared memory by one weight tile. That went unnoticed at the default chunk size, and is an illegal memory access for some njmax below the chunk size, where the chunk is njmax rows. The weights are now computed in the kernels, which also drops the copy of the returned tile.
The dense cone curvature is built from the virtual rows that the constraint update writes, so rescaling Jaref and only updating the gradient compared two Hessians built from the same rows. The test now updates the constraints after rescaling.
The cone row rewrite indexed a shared-memory copy of the chunk's constraint states, which every row chunk loaded whether or not it had cone rows. The rewrite now reads the state of a row from efc.state, and the chunk's state tile stays in registers.
|
Thanks @thowell, all four are addressed in a7edee1, on top of a merge of current main.
Two more things came up while testing these:
While there, a9bcc21 makes the cone row rewrite read a row's state from The full suite passes on an L40. Re-measured on an RTX PRO 6000 Blackwell at 8192 worlds (median of 5 interleaved runs, steps/s) against main at d3f27d6 and against this PR before these commits:
|
…enzler/dense-elliptic-fused
The dense Hessian kernel gained an nv argument on main, and test_elliptic_dense_hessian launches it directly.
…ptic-fused # Conflicts: # mujoco_warp/_src/io.py # mujoco_warp/_src/solver.py # mujoco_warp/_src/types.py
Dense Newton builds the elliptic cone curvature in a separate kernel,
_update_gradient_JTCJ_dense, that strides over every contact in the batch for every upper-triangle Hessian entry and accumulates into the Hessian with atomics. Every solver pass pays that scan, including the late passes in which almost every world has already converged, and each contact's Jacobian rows are reloaded once per Hessian entry.The cone curvature of a contact is diagonal plus rank one, so it can be written as a sum of squared, weighted combinations of the contact's own Jacobian rows: the normal direction minus the projected tangent, and an orthonormal (Householder) basis of the tangent space orthogonal to the sliding direction. Each cone-state row maps to one such virtual row and weight, and the existing per-world J'WJ tile product of
_update_gradient_JTDAJ_dense_tiledthen assembles the whole Hessian. This mirrors the sparse path, which already folds the elliptic curvature into its per-group assembly (#1521).wp.emptyand exist only for dense elliptic Newton.efc.state. Pyramidal and other dense models compile the unchanged code, apart from clampingnefctonjmax._update_gradient_JTCJ_denseand its SM-count based launch sizing are removed, along withdof_tri_row/dof_tri_colonModelandcdof_tri_row/cdof_tri_colonData, which only it read. The dense Hessian is built by one per-world kernel without atomics, and converged worlds exit at the start of the block.dist >= includemargin, but contact rows only exist inside the include margin (adhesive contacts use the full margin), so that check never applied.test_elliptic_dense_hessiannow launches the tile kernel with and without the elliptic term and checks the difference against the independent NumPy reference for mixed condim 3/4/6 contacts, for one and two heterogeneous worlds.test_constrained_solve_equivalence_all_activealso covers dense Jacobians with sliding contacts; for elliptic cones it takes a single Newton step from the warmstart, whereqaccdepends on the cone Hessian of the compact kernel.test_elliptic_truncated_contactcovers a contact truncated bynjmax, andtest_elliptic_dense_njmax_below_chunkannjmaxbelow the row chunk size.The three context arrays add 32 bytes per constraint row for dense elliptic Newton only and are allocated with
wp.empty: the weights are padded tonjmax_padlikeefc.D, and rows pastnefcare masked before a cone weight is read. The constraint update now gives the rows of an elliptic contact truncated by annjmaxornaconmaxoverflow zero force and the satisfied state instead of leaving both stale, so every cone-state row has its coefficients written in the same pass.Benchmarks on an RTX PRO 6000 Blackwell (MIG 2g.48gb slice) against
24e6d81b, interleaved runs, zero overflow and all worlds converged. The elliptic benchmarks inbenchmarks/run.py(median of 4 main and 3 PR runs);aloha_clutteruses the sparse Jacobian and is unaffected:mjwarp-testspeed, nworld 8192, nstep 1000, median of 5 interleaved runs, time per step:opt.cone=ELLIPTIC opt.jacobian=DENSEopt.cone=ELLIPTIC opt.jacobian=DENSESolver iteration counts are unchanged.
Re-measured after the review changes on a full RTX PRO 6000 Blackwell against main at
d3f27d64,mjwarp-testspeedwith nworld 8192 and nstep 1000, median of 5 interleaved runs, steps per second:opt.cone=ELLIPTIC opt.jacobian=DENSEopt.cone=ELLIPTIC opt.jacobian=DENSEopt.jacobian=DENSE(pyramidal)