Skip to content

Commit 10eeb82

Browse files
yuvaltassacopybara-github
authored andcommitted
Fold cone contributions into the Hessian when full factorization is faster than multiple rank-1 updates.
In the elliptic Newton solver, HessianCone rebuilds Lcone every iteration by copying L and applying dim sparse rank-1 updates per cone-state contact. With many sliding contacts the updates dwarf the factorization they avoid. Since a cone contact's contribution Jc'*Hc*Jc equals (Lc'*Jc)'(Lc'*Jc), replacing its Jacobian rows with Lc'*Jc at unit weight lets the existing J'DJ + factorize pipeline produce the cone-inclusive Hessian in one factorization, with unchanged sparsity. PiperOrigin-RevId: 976196961 Change-Id: I42fb6bad724af96abc4373ef0b913fb7bd16567e
1 parent f481f0b commit 10eeb82

6 files changed

Lines changed: 445 additions & 4 deletions

File tree

‎doc/changelog.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Engine
3838
since then, models with singular mass matrices silently produced non-finite accelerations, typically surfacing as
3939
divergence warnings and automatic resets.
4040
- Added single-shot :ref:`multicontact<coMultiCCD>` for collisions with cylinder geoms.
41+
- The Newton solver with :ref:`elliptic cones<option-cone>` now rebuilds the cone-augmented Hessian factor with a
42+
single refactorization instead of per-contact rank-1 updates when a flop-count model predicts this is faster.
43+
Scenes with many simultaneously sliding contacts speed up by 1.4-2x on average and 3-4x on the slowest steps.
44+
Contribution by :github:user:`kevinzakka`.
4145

4246
Compiler
4347
^^^^^^^^

‎src/engine/engine_solver.c‎

Lines changed: 158 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,20 @@ typedef struct {
10961096
int ncone; // number of contacts in cone state
10971097
int nupdate; // number of Cholesky updates
10981098

1099+
// cone fold gate (elliptic sparse)
1100+
int* pathcost; // reverse-etree path costs of L rows (nv x 1)
1101+
mjtSize Lflops; // sum of squared L row nonzeros (2x factorization MACs)
1102+
int cone_fold; // cached fold decision (-1: recompute)
1103+
1104+
// cone fold workspace, allocated on the first fold (elliptic sparse)
1105+
mjtNum* Jmod; // Jacobian, cone rows replaced by Lc'*Jc (nJ x 1)
1106+
mjtNum* JTmod; // transpose of Jmod (nJ x 1)
1107+
mjtNum* Dmod; // constraint inertia, cone rows set to 1 (nefc x 1)
1108+
mjtNum* Hcone; // cone-inclusive Hessian (nH x 1)
1109+
int* JTmod_rownnz; // Jmod transpose row nonzeros (nv x 1)
1110+
int* JTmod_rowadr; // Jmod transpose row addresses (nv x 1)
1111+
int* JTmod_colind; // Jmod transpose column indices (nJ x 1)
1112+
10991113
// linesearch diagnostics
11001114
int LSiter; // number of linesearch iterations
11011115
int LSresult; // linesearch result
@@ -1113,6 +1127,7 @@ static void PrimalPointers(const mjModel* m, const mjData* d, mjPrimalContext* c
11131127
ctx->is_elliptic = (m->opt.cone == mjCONE_ELLIPTIC);
11141128
ctx->contact = d->contact;
11151129
ctx->island = island;
1130+
ctx->cone_fold = -1;
11161131

11171132
// set sizes and pointers (monolithic)
11181133
if (island < 0) {
@@ -2365,13 +2380,14 @@ static void MakeHessian(mjData* d, mjPrimalContext* ctx) {
23652380
nv, d);
23662381

23672382
// nL is known: allocate blocks and carve L_colind, LT_colind, LT_map, L, Lcone
2368-
size_t nL_int = 2*ctx->nL + ctx->nL; // L_colind + LT_colind + LT_map
2369-
size_t nL_num = ctx->is_elliptic ? 2*ctx->nL : ctx->nL; // L + Lcone
2383+
size_t nL_int = 3*ctx->nL + (ctx->is_elliptic ? nv : 0); // L_colind + LT_colind + LT_map + pathcost
2384+
size_t nL_num = ctx->is_elliptic ? 2*ctx->nL : ctx->nL; // L + Lcone
23702385
int* L_intblock = mjSTACKALLOC(d, nL_int, int);
23712386
mjtNum* L_numblock = mjSTACKALLOC(d, nL_num, mjtNum);
23722387
ctx->L_colind = L_intblock;
23732388
ctx->LT_colind = L_intblock + ctx->nL;
23742389
ctx->LT_map = L_intblock + 2*ctx->nL;
2390+
ctx->pathcost = ctx->is_elliptic ? L_intblock + 3*ctx->nL : NULL;
23752391
ctx->L = L_numblock;
23762392
ctx->Lcone = ctx->is_elliptic ? L_numblock + ctx->nL : NULL;
23772393

@@ -2380,6 +2396,20 @@ static void MakeHessian(mjData* d, mjPrimalContext* ctx) {
23802396
ctx->LT_colind, ctx->LT_rownnz, ctx->LT_rowadr, ctx->LT_map,
23812397
ctx->HT_rownnz, ctx->HT_rowadr, ctx->HT_colind,
23822398
nv, d);
2399+
2400+
// cone fold gate inputs, fixed for the solve since the L pattern is fixed:
2401+
// pathcost[r] totals the L row nonzeros along the reverse-etree path from
2402+
// row r (parent(r) = largest off-diagonal column of L row r), the cost of
2403+
// a rank-1 update starting at column r; Lflops totals squared row nonzeros
2404+
if (ctx->is_elliptic) {
2405+
ctx->Lflops = 0;
2406+
for (int r=0; r < nv; r++) {
2407+
int nnz = ctx->L_rownnz[r];
2408+
int parent = nnz >= 2 ? ctx->L_colind[ctx->L_rowadr[r] + nnz - 2] : -1;
2409+
ctx->pathcost[r] = nnz + (parent >= 0 ? ctx->pathcost[parent] : 0);
2410+
ctx->Lflops += (mjtSize)nnz * nnz;
2411+
}
2412+
}
23832413
}
23842414

23852415
// dense
@@ -2476,6 +2506,7 @@ static void FactorizeHessian(mjData* d, mjPrimalContext* ctx, int flg_recompute)
24762506

24772507
// add cones to factor if present
24782508
if (ctx->ncone) {
2509+
ctx->cone_fold = -1;
24792510
HessianCone(d, ctx);
24802511
}
24812512

@@ -2484,8 +2515,80 @@ static void FactorizeHessian(mjData* d, mjPrimalContext* ctx, int flg_recompute)
24842515
}
24852516

24862517

2487-
// elliptic case: Hcone = H + cone_contributions
2488-
static void HessianCone(mjData* d, mjPrimalContext* ctx) {
2518+
// elliptic sparse case with many cones: rebuild Lcone with one factorization.
2519+
static void HessianConeFolded(mjData* d, mjPrimalContext* ctx) {
2520+
int nv = ctx->nv, nefc = ctx->nefc;
2521+
mjtNum local[36];
2522+
2523+
// allocate on the first fold of this solve and reuse for the rest of it
2524+
if (!ctx->Jmod) {
2525+
ctx->Jmod = mjSTACKALLOC(d, ctx->nJ, mjtNum);
2526+
ctx->JTmod = mjSTACKALLOC(d, ctx->nJ, mjtNum);
2527+
ctx->Dmod = mjSTACKALLOC(d, nefc, mjtNum);
2528+
ctx->Hcone = mjSTACKALLOC(d, ctx->nH, mjtNum);
2529+
ctx->JTmod_rownnz = mjSTACKALLOC(d, nv, int);
2530+
ctx->JTmod_rowadr = mjSTACKALLOC(d, nv, int);
2531+
ctx->JTmod_colind = mjSTACKALLOC(d, ctx->nJ, int);
2532+
}
2533+
mjtNum* Jmod = ctx->Jmod;
2534+
mjtNum* JTmod = ctx->JTmod;
2535+
mjtNum* Dmod = ctx->Dmod;
2536+
mjtNum* Hcone = ctx->Hcone;
2537+
int* JT_rownnz = ctx->JTmod_rownnz;
2538+
int* JT_rowadr = ctx->JTmod_rowadr;
2539+
int* JT_colind = ctx->JTmod_colind;
2540+
2541+
mju_copy(Jmod, ctx->J, ctx->nJ);
2542+
for (int i=0; i < nefc; i++) {
2543+
Dmod[i] = ctx->efc_state[i] == mjCNSTRSTATE_QUADRATIC ? ctx->efc_D[i] : 0;
2544+
}
2545+
2546+
// replace cone contact rows with Lc'*Jc, where Hc = Lc*Lc'
2547+
for (int i=0; i < nefc; i++) {
2548+
if (ctx->efc_state[i] == mjCNSTRSTATE_CONE) {
2549+
int dim = ctx->contact[ctx->efc_id[i]].dim;
2550+
mju_copy(local, ctx->contact[ctx->efc_id[i]].H, dim*dim);
2551+
mju_cholFactor(local, dim, mjMINVAL);
2552+
2553+
const int nnz = ctx->J_rownnz[i];
2554+
for (int c=0; c < dim; c++) {
2555+
mjtNum* dst = Jmod + ctx->J_rowadr[i+c];
2556+
mju_zero(dst, nnz);
2557+
for (int r=c; r < dim; r++) {
2558+
mju_addToScl(dst, ctx->J + ctx->J_rowadr[i+r], local[r*dim+c], nnz);
2559+
}
2560+
Dmod[i+c] = 1;
2561+
}
2562+
ctx->nupdate += dim;
2563+
i += (dim-1);
2564+
}
2565+
}
2566+
2567+
// Lcone = chol(Jmod'*Dmod*Jmod + M), same sparsity as L
2568+
mju_transposeSparse(JTmod, Jmod, nefc, nv, JT_rownnz, JT_rowadr, JT_colind, NULL,
2569+
ctx->J_rownnz, ctx->J_rowadr, ctx->J_colind);
2570+
mju_sqrMatTDSparseNumeric(Hcone, nv, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, NULL,
2571+
Jmod, ctx->J_rownnz, ctx->J_rowadr, ctx->J_colind,
2572+
JTmod, JT_rownnz, JT_rowadr, JT_colind,
2573+
ctx->JT_rowsuper, Dmod, d);
2574+
mju_addToMatSparse(Hcone, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, nv,
2575+
ctx->M, ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind);
2576+
if (ctx->nS) {
2577+
mju_addToMatSparse(Hcone, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, nv,
2578+
ctx->S_val, ctx->S_rownnz, ctx->S_rowadr, ctx->S_colind);
2579+
}
2580+
if (mju_cholFactorNumeric(ctx->Lcone, nv, mjMINVAL,
2581+
ctx->L_rownnz, ctx->L_rowadr, ctx->L_colind,
2582+
ctx->LT_rownnz, ctx->LT_rowadr, ctx->LT_colind, ctx->LT_map,
2583+
Hcone, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind,
2584+
ctx->cholscratch) != nv) {
2585+
mjERROR("rank-deficient cone Hessian");
2586+
}
2587+
}
2588+
2589+
2590+
// elliptic case, update path: Lcone = L, then dim rank-1 updates per cone contact
2591+
static void HessianConeUpdate(mjData* d, mjPrimalContext* ctx) {
24892592
int nv = ctx->nv, nefc = ctx->nefc;
24902593
mjtNum* LTJ = ctx->LTJ;
24912594
mjtNum local[36];
@@ -2550,6 +2653,52 @@ static void HessianCone(mjData* d, mjPrimalContext* ctx) {
25502653
}
25512654

25522655

2656+
// predict whether folding is faster than rank-1 updates for the current set, comparing flop counts:
2657+
// update path: U = sum over cone contacts of dim * pathcost of the contact's last dof (a rank-1
2658+
// update walks the reverse-etree path of its last dof; lower dofs merge into it)
2659+
// folded path: Lflops/2 for the factorization plus S for J'*D*J over the D != 0 rows
2660+
// fold when 10 U > 3 (Lflops + 2 S): an update flop costs about twice a factorization flop
2661+
static int HessianConeFoldFaster(const mjPrimalContext* ctx) {
2662+
int nefc = ctx->nefc;
2663+
2664+
// U: update flops, S: J'*D*J flops
2665+
mjtSize U = 0, S = 0;
2666+
for (int i=0; i < nefc; i++) {
2667+
int nnz = ctx->J_rownnz[i];
2668+
if (ctx->efc_state[i] == mjCNSTRSTATE_QUADRATIC) {
2669+
S += (mjtSize)nnz * nnz;
2670+
} else if (ctx->efc_state[i] == mjCNSTRSTATE_CONE) {
2671+
int dim = ctx->contact[ctx->efc_id[i]].dim;
2672+
int lastcol = ctx->J_colind[ctx->J_rowadr[i] + nnz - 1];
2673+
U += (mjtSize)dim * ctx->pathcost[lastcol];
2674+
S += (mjtSize)dim * nnz * nnz;
2675+
i += dim - 1;
2676+
}
2677+
}
2678+
2679+
return 10*U > 3*(ctx->Lflops + 2*S);
2680+
}
2681+
2682+
2683+
// elliptic case: Hcone = H + cone_contributions
2684+
static void HessianCone(mjData* d, mjPrimalContext* ctx) {
2685+
// sparse mode with many sliding contacts: one refactorization can beat
2686+
// sum(dim) rank-1 updates per cone contact. The decision depends only on
2687+
// sparsity patterns and efc_state, so it is cached until a state change.
2688+
if (ctx->is_sparse) {
2689+
if (ctx->cone_fold < 0) {
2690+
ctx->cone_fold = HessianConeFoldFaster(ctx);
2691+
}
2692+
if (ctx->cone_fold) {
2693+
HessianConeFolded(d, ctx);
2694+
return;
2695+
}
2696+
}
2697+
2698+
HessianConeUpdate(d, ctx);
2699+
}
2700+
2701+
25532702
// incremental update to Hessian factor due to changes in efc_state
25542703
static void HessianIncremental(mjData* d, mjPrimalContext* ctx, const int* oldstate) {
25552704
int rank, nv = ctx->nv, nefc = ctx->nefc;
@@ -2562,6 +2711,11 @@ static void HessianIncremental(mjData* d, mjPrimalContext* ctx, const int* oldst
25622711
for (int i=0; i < nefc; i++) {
25632712
int flag_update = -1;
25642713

2714+
// any state change invalidates the cached cone fold decision
2715+
if (oldstate[i] != ctx->efc_state[i]) {
2716+
ctx->cone_fold = -1;
2717+
}
2718+
25652719
// add quad
25662720
if (oldstate[i] != mjCNSTRSTATE_QUADRATIC && ctx->efc_state[i] == mjCNSTRSTATE_QUADRATIC) {
25672721
flag_update = 1;

‎test/benchmark/CMakeLists.txt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ mujoco_test(
1919
ADDITIONAL_LINK_LIBRARIES benchmark::benchmark absl::core_headers ccd
2020
)
2121

22+
mujoco_test(
23+
cone_benchmark_test
24+
MAIN_TARGET benchmark::benchmark_main
25+
ADDITIONAL_LINK_LIBRARIES benchmark::benchmark absl::core_headers
26+
)
27+
2228
mujoco_test(
2329
step_benchmark_test
2430
MAIN_TARGET benchmark::benchmark_main
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2026 DeepMind Technologies Limited
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// A benchmark for the elliptic Newton solver with many cone-state contacts.
16+
17+
#include <benchmark/benchmark.h>
18+
#include <absl/base/attributes.h>
19+
#include <mujoco/mujoco.h>
20+
#include "test/fixture.h"
21+
22+
namespace mujoco {
23+
namespace {
24+
25+
// A pile of boxes sliding under diagonal gravity, keyframed in two regimes:
26+
// "clump" is piled in a corner with a coupled contact graph, where the solver
27+
// folds the cone contributions into the Hessian with one refactorization;
28+
// "scatter" is independent stacks, where per-contact rank-1 updates are
29+
// cheaper (despite holding more cone-state rows than the clump). Together the
30+
// two keep both HessianCone paths, and the flop-count gate between them,
31+
// under measurement.
32+
static void run_cone_benchmark(benchmark::State& state, const char* key) {
33+
MujocoErrorTestGuard guard;
34+
static mjModel* model =
35+
LoadModelFromPath("../test/benchmark/testdata/boxpile.xml");
36+
mjData* data = mj_makeData(model);
37+
int keyid = mj_name2id(model, mjOBJ_KEY, key);
38+
39+
for (auto s : state) {
40+
mj_resetDataKeyframe(model, data, keyid);
41+
mj_forward(model, data);
42+
}
43+
44+
mj_deleteData(data);
45+
state.SetItemsProcessed(state.iterations());
46+
}
47+
48+
void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_ConeClump(benchmark::State& state) {
49+
run_cone_benchmark(state, "clump");
50+
}
51+
BENCHMARK(BM_ConeClump);
52+
53+
void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_ConeScatter(benchmark::State& state) {
54+
run_cone_benchmark(state, "scatter");
55+
}
56+
BENCHMARK(BM_ConeScatter);
57+
58+
} // namespace
59+
} // namespace mujoco

0 commit comments

Comments
 (0)
⌂ 换网址