Files
Mujoco_WASM/test/benchmark/chol_benchmark_test.cc
T
Yuval Tassa e6d77650f7 Refactor mju_combineSparse to eliminate temporary buffers.
Combine sparse vectors in-place by first counting total `nnz` and then working backwards from the end. This removes the need for temporary buffers in `mju_combineSparse` and its callers and speeds up the function by ~10%.

PiperOrigin-RevId: 902530210
Change-Id: I4f48c327103552ab968d3915399c6067367bec9f
2026-04-20 03:10:18 -07:00

527 lines
17 KiB
C++

// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// A benchmark for comparing old vs new Cholesky factorization implementations.
#include <algorithm>
#include <cstring>
#include <vector>
#include <benchmark/benchmark.h>
#include <absl/base/attributes.h>
#include <mujoco/mjdata.h>
#include <mujoco/mujoco.h>
#include "src/engine/engine_support.h"
#include "src/engine/engine_util_solve.h"
#include "src/engine/engine_util_sparse.h"
#include "test/fixture.h"
namespace mujoco {
namespace {
// Helper to compute H = M + J'*D*J using sparse matrices
struct HessianData {
int nv;
int nL;
int nH;
// H sparse structure
std::vector<mjtNum> H;
std::vector<int> H_rownnz;
std::vector<int> H_rowadr;
std::vector<int> H_colind;
// H transpose for symbolics
std::vector<int> HT_rownnz;
std::vector<int> HT_rowadr;
std::vector<int> HT_colind;
// L factor structure
std::vector<int> L_rownnz;
std::vector<int> L_rowadr;
// L initial values for BM_chol_old (lower triangle of H, zero-filled for
// fill-in)
std::vector<mjtNum> L_init;
std::vector<int> L_rownnz_init;
std::vector<int> L_colind_init;
// J transpose
std::vector<mjtNum> JT;
std::vector<int> JT_rownnz;
std::vector<int> JT_rowadr;
std::vector<int> JT_colind;
std::vector<int> JT_rowsuper;
// D diagonal
std::vector<mjtNum> D;
int nefc;
void Setup(const mjModel* m, mjData* d) {
// initialize simulation state
mj_resetDataKeyframe(m, d, 0);
mj_forward(m, d);
nv = m->nv;
nefc = d->nefc;
// compute D corresponding to quad states
D.resize(nefc);
for (int i = 0; i < nefc; i++) {
if (d->efc_state[i] == mjCNSTRSTATE_QUADRATIC) {
D[i] = d->efc_D[i];
} else {
D[i] = 0;
}
}
// transpose J
JT.resize(d->nJ);
JT_rownnz.resize(nv);
JT_rowadr.resize(nv);
JT_colind.resize(d->nJ);
JT_rowsuper.resize(nv);
mju_transposeSparse(JT.data(), d->efc_J, nefc, nv, JT_rownnz.data(),
JT_rowadr.data(), JT_colind.data(), JT_rowsuper.data(),
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind);
// count H sparsity: nH from J'*D*J
H_rownnz.resize(nv);
H_rowadr.resize(nv);
mju_sqrMatTDSparseCount(H_rownnz.data(), H_rowadr.data(), nv,
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind,
JT_rownnz.data(), JT_rowadr.data(),
JT_colind.data(), nullptr, d, 1);
// add M elements to the H row counts and addresses
for (int r = 0; r < nv; r++) {
H_rownnz[r] += m->M_rownnz[r];
}
H_rowadr[0] = 0;
for (int r = 1; r < nv; r++) {
H_rowadr[r] = H_rowadr[r - 1] + H_rownnz[r - 1];
}
nH = H_rowadr[nv - 1] + H_rownnz[nv - 1];
// allocate H and colind with proper sparse size, zero-initialize H
H.assign(nH, 0);
H_colind.assign(nH, 0);
// reset rownnz for filling (sqrMatTDSparse will fill it again)
std::fill(H_rownnz.begin(), H_rownnz.end(), 0);
// recount just J'*D*J (without M shift)
mju_sqrMatTDSparseCount(H_rownnz.data(), H_rowadr.data(), nv,
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind,
JT_rownnz.data(), JT_rowadr.data(),
JT_colind.data(), nullptr, d, 1);
// add shift for M to rowadr
int shift = 0;
for (int r = 0; r < nv - 1; r++) {
shift += m->M_rownnz[r];
H_rowadr[r + 1] += shift;
}
// compute H = J'*D*J
mju_sqrMatTDSparse(H.data(), d->efc_J, JT.data(), D.data(), nefc, nv,
H_rownnz.data(), H_rowadr.data(), H_colind.data(),
d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind,
nullptr, JT_rownnz.data(), JT_rowadr.data(),
JT_colind.data(), JT_rowsuper.data(), d, nullptr);
// add M to H using mj_addM
mj_addM(m, d, H.data(), H_rownnz.data(), H_rowadr.data(), H_colind.data());
// transpose H for symbolic
HT_rownnz.resize(nv);
HT_rowadr.resize(nv);
HT_colind.resize(nH);
mju_transposeSparse(nullptr, nullptr, nv, nv, HT_rownnz.data(),
HT_rowadr.data(), HT_colind.data(), nullptr,
H_rownnz.data(), H_rowadr.data(), H_colind.data());
// count L fill-in (also counts LT structure)
L_rownnz.resize(nv);
L_rowadr.resize(nv);
std::vector<int> LT_rownnz_temp(nv);
std::vector<int> LT_rowadr_temp(nv);
nL = mju_cholFactorSymbolic(
nullptr, L_rownnz.data(), L_rowadr.data(), nullptr,
LT_rownnz_temp.data(), LT_rowadr_temp.data(), nullptr, HT_rownnz.data(),
HT_rowadr.data(), HT_colind.data(), nv, d);
// precompute initial L state for BM_chol_old
// extract lower triangle of H into L format, zero-fill for fill-in
L_init.assign(nL, 0);
L_colind_init.assign(nL, 0);
L_rownnz_init.resize(nv);
for (int r = 0; r < nv; r++) {
int l_adr = L_rowadr[r];
int h_adr = H_rowadr[r];
int lower_nnz = 0;
for (int i = 0; i < H_rownnz[r]; i++) {
int col = H_colind[h_adr + i];
if (col <= r) {
L_init[l_adr + lower_nnz] = H[h_adr + i];
L_colind_init[l_adr + lower_nnz] = col;
lower_nnz++;
}
}
L_rownnz_init[r] = lower_nnz;
}
}
};
// ----------------------------- benchmark ------------------------------------
enum class Size { L, XL };
template <Size S>
const char* ModelPath() {
if constexpr (S == Size::L) {
return "../test/benchmark/testdata/2humanoid100_chol.xml";
} else {
return "../test/benchmark/testdata/100_humanoids_chol.xml";
}
}
template <Size S>
mjModel* GetModel() {
static mjModel* m = LoadModelFromPath(ModelPath<S>());
m->opt.jacobian = mjJAC_SPARSE;
m->opt.solver = mjSOL_NEWTON;
return m;
}
template <Size S>
HessianData& GetHessianData() {
static HessianData data;
static bool initialized = false;
if (!initialized) {
mjModel* m = GetModel<S>();
mjData* d = mj_makeData(m);
data.Setup(m, d);
mj_deleteData(d);
initialized = true;
}
return data;
}
// old implementation benchmark
template <Size S>
static void BM_chol_old(benchmark::State& state) {
mjModel* m = GetModel<S>();
mjData* d = mj_makeData(m);
HessianData& hd = GetHessianData<S>();
std::vector<mjtNum> L_work(hd.nL);
std::vector<int> L_colind_work(hd.nL);
std::vector<int> L_rownnz_work(hd.nv);
for (auto s : state) {
// fast reset using memcpy from precomputed initial state
std::memcpy(L_work.data(), hd.L_init.data(), hd.nL * sizeof(mjtNum));
std::memcpy(L_colind_work.data(), hd.L_colind_init.data(),
hd.nL * sizeof(int));
std::memcpy(L_rownnz_work.data(), hd.L_rownnz_init.data(),
hd.nv * sizeof(int));
mju_cholFactorSparse(L_work.data(), hd.nv, mjMINVAL, L_rownnz_work.data(),
hd.L_rowadr.data(), L_colind_work.data(), d);
}
mj_deleteData(d);
state.SetItemsProcessed(state.iterations());
}
// new symbolic implementation benchmark
template <Size S>
static void BM_chol_symbolic(benchmark::State& state) {
mjModel* m = GetModel<S>();
mjData* d = mj_makeData(m);
HessianData& hd = GetHessianData<S>();
std::vector<int> L_colind_work(hd.nL);
std::vector<int> LT_rownnz_work(hd.nv);
std::vector<int> LT_rowadr_work(hd.nv);
std::vector<int> LT_colind_work(hd.nL);
std::vector<int> LT_pos_work(hd.nL);
for (auto s : state) {
mju_cholFactorSymbolic(L_colind_work.data(), hd.L_rownnz.data(),
hd.L_rowadr.data(), LT_colind_work.data(),
LT_rownnz_work.data(), LT_rowadr_work.data(),
LT_pos_work.data(), hd.HT_rownnz.data(),
hd.HT_rowadr.data(), hd.HT_colind.data(), hd.nv, d);
}
mj_deleteData(d);
state.SetItemsProcessed(state.iterations());
}
// new numeric implementation benchmark
template <Size S>
static void BM_chol_numeric(benchmark::State& state) {
mjModel* m = GetModel<S>();
mjData* d = mj_makeData(m);
HessianData& hd = GetHessianData<S>();
std::vector<mjtNum> L_work(hd.nL);
std::vector<int> L_colind_work(hd.nL);
std::vector<int> LT_rownnz_work(hd.nv);
std::vector<int> LT_rowadr_work(hd.nv);
std::vector<int> LT_colind_work(hd.nL);
std::vector<int> LT_pos_work(hd.nL);
// symbolic setup (not benchmarked)
mju_cholFactorSymbolic(L_colind_work.data(), hd.L_rownnz.data(),
hd.L_rowadr.data(), LT_colind_work.data(),
LT_rownnz_work.data(), LT_rowadr_work.data(),
LT_pos_work.data(), hd.HT_rownnz.data(),
hd.HT_rowadr.data(), hd.HT_colind.data(), hd.nv, d);
for (auto s : state) {
mju_cholFactorNumeric(
L_work.data(), hd.nv, mjMINVAL, hd.L_rownnz.data(), hd.L_rowadr.data(),
L_colind_work.data(), LT_rownnz_work.data(), LT_rowadr_work.data(),
LT_colind_work.data(), LT_pos_work.data(), hd.H.data(),
hd.H_rownnz.data(), hd.H_rowadr.data(), hd.H_colind.data(), d);
}
mj_deleteData(d);
state.SetItemsProcessed(state.iterations());
}
void BM_old_L(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_chol_old<Size::L>(state);
}
BENCHMARK(BM_old_L);
void BM_symbolic_L(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_chol_symbolic<Size::L>(state);
}
BENCHMARK(BM_symbolic_L);
void BM_numeric_L(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_chol_numeric<Size::L>(state);
}
BENCHMARK(BM_numeric_L);
void BM_old_XL(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_chol_old<Size::XL>(state);
}
BENCHMARK(BM_old_XL);
void BM_symbolic_XL(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_chol_symbolic<Size::XL>(state);
}
BENCHMARK(BM_symbolic_XL);
void BM_numeric_XL(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_chol_numeric<Size::XL>(state);
}
BENCHMARK(BM_numeric_XL);
// -------------------- rank-1 update/downdate benchmarks ----------------------
constexpr int kNumUpdateVectors = 25;
// old implementation using sparse merge
int ABSL_ATTRIBUTE_NOINLINE mju_cholUpdateSparse_old(
mjtNum* mat, mjtNum* x, int n, int flg_plus, const int* rownnz,
const int* rowadr, const int* colind, int x_nnz, int* x_ind, mjData* d) {
int rank = n, i = x_nnz - 1;
while (i >= 0) {
int nnz = rownnz[x_ind[i]], adr = rowadr[x_ind[i]];
mjtNum tmp = mat[adr + nnz - 1] * mat[adr + nnz - 1] +
(flg_plus ? x[i] * x[i] : -x[i] * x[i]);
if (tmp < mjMINVAL) {
tmp = mjMINVAL;
rank--;
}
mjtNum r = mju_sqrt(tmp);
mjtNum c = r / mat[adr + nnz - 1];
mjtNum s = x[i] / mat[adr + nnz - 1];
mat[adr + nnz - 1] = r;
mju_combineSparseInc(mat + adr, x, n, 1 / c, (flg_plus ? s / c : -s / c),
nnz - 1, i, colind + adr, x_ind);
int new_x_nnz = mju_combineSparse(x, mat + adr, c, -s, i, nnz - 1, x_ind,
colind + adr);
i = i - 1 + (new_x_nnz - i);
}
return rank;
}
// old update implementation benchmark
template <Size S>
static void BM_update_old(benchmark::State& state) {
mjModel* m = GetModel<S>();
mjData* d = mj_makeData(m);
mj_resetDataKeyframe(m, d, 0);
mj_forward(m, d);
HessianData& hd = GetHessianData<S>();
int nv = hd.nv;
// factorize L using old method (we'll just keep updating this)
std::vector<mjtNum> L_work(hd.nL);
std::vector<int> L_colind_work(hd.nL);
std::vector<int> L_rownnz_work(hd.nv);
std::memcpy(L_work.data(), hd.L_init.data(), hd.nL * sizeof(mjtNum));
std::memcpy(L_colind_work.data(), hd.L_colind_init.data(),
hd.nL * sizeof(int));
std::memcpy(L_rownnz_work.data(), hd.L_rownnz_init.data(),
hd.nv * sizeof(int));
mju_cholFactorSparse(L_work.data(), nv, mjMINVAL, L_rownnz_work.data(),
hd.L_rowadr.data(), L_colind_work.data(), d);
// prepare update vectors: pick kNumUpdateVectors rows from J (constraint
// rows) Each J row has DoF indices which is correct for updating L (nv x nv)
int nefc = d->nefc;
std::vector<std::vector<mjtNum>> update_vecs(kNumUpdateVectors);
std::vector<std::vector<int>> update_inds(kNumUpdateVectors);
for (int k = 0; k < kNumUpdateVectors; k++) {
int row = (k * 7) % nefc;
int nnz = d->efc_J_rownnz[row];
int adr = d->efc_J_rowadr[row];
update_vecs[k].resize(nnz);
update_inds[k].resize(nnz);
for (int i = 0; i < nnz; i++) {
update_vecs[k][i] = d->efc_J[adr + i] * 0.01;
update_inds[k][i] = d->efc_J_colind[adr + i];
}
}
// working copy of update vector (sized to nv since pattern can grow)
std::vector<mjtNum> x_work(nv);
std::vector<int> x_ind_work(nv);
int vec_idx = 0;
for (auto s : state) {
int nnz = update_inds[vec_idx].size();
std::memset(x_work.data(), 0, nv * sizeof(mjtNum));
std::memcpy(x_work.data(), update_vecs[vec_idx].data(),
nnz * sizeof(mjtNum));
std::memcpy(x_ind_work.data(), update_inds[vec_idx].data(),
nnz * sizeof(int));
mju_cholUpdateSparse_old(L_work.data(), x_work.data(), nv, 1,
L_rownnz_work.data(), hd.L_rowadr.data(),
L_colind_work.data(), nnz, x_ind_work.data(), d);
vec_idx = (vec_idx + 1) % kNumUpdateVectors;
}
mj_deleteData(d);
state.SetItemsProcessed(state.iterations());
}
// new update implementation benchmark
template <Size S>
static void BM_update_new(benchmark::State& state) {
mjModel* m = GetModel<S>();
mjData* d = mj_makeData(m);
mj_resetDataKeyframe(m, d, 0);
mj_forward(m, d);
HessianData& hd = GetHessianData<S>();
int nv = hd.nv;
// factorize L using new method
std::vector<mjtNum> L_work(hd.nL);
std::vector<int> L_colind_work(hd.nL);
std::vector<int> LT_rownnz_work(nv);
std::vector<int> LT_rowadr_work(nv);
std::vector<int> LT_colind_work(hd.nL);
std::vector<int> LT_pos_work(hd.nL);
mju_cholFactorSymbolic(L_colind_work.data(), hd.L_rownnz.data(),
hd.L_rowadr.data(), LT_colind_work.data(),
LT_rownnz_work.data(), LT_rowadr_work.data(),
LT_pos_work.data(), hd.HT_rownnz.data(),
hd.HT_rowadr.data(), hd.HT_colind.data(), nv, d);
mju_cholFactorNumeric(
L_work.data(), nv, mjMINVAL, hd.L_rownnz.data(), hd.L_rowadr.data(),
L_colind_work.data(), LT_rownnz_work.data(), LT_rowadr_work.data(),
LT_colind_work.data(), LT_pos_work.data(), hd.H.data(),
hd.H_rownnz.data(), hd.H_rowadr.data(), hd.H_colind.data(), d);
// prepare update vectors: pick kNumUpdateVectors rows from J (constraint
// rows) Each J row has DoF indices which is correct for updating L (nv x nv)
int nefc = d->nefc;
std::vector<std::vector<mjtNum>> update_vecs(kNumUpdateVectors);
std::vector<std::vector<int>> update_inds(kNumUpdateVectors);
for (int k = 0; k < kNumUpdateVectors; k++) {
int row = (k * 7) % nefc;
int nnz = d->efc_J_rownnz[row];
int adr = d->efc_J_rowadr[row];
update_vecs[k].resize(nnz);
update_inds[k].resize(nnz);
for (int i = 0; i < nnz; i++) {
update_vecs[k][i] = d->efc_J[adr + i] * 0.01;
update_inds[k][i] = d->efc_J_colind[adr + i];
}
}
int vec_idx = 0;
for (auto s : state) {
int nnz = update_inds[vec_idx].size();
mju_cholUpdateSparse(L_work.data(), update_vecs[vec_idx].data(), nv, 1,
hd.L_rownnz.data(), hd.L_rowadr.data(),
L_colind_work.data(), nnz, update_inds[vec_idx].data(),
d);
vec_idx = (vec_idx + 1) % kNumUpdateVectors;
}
mj_deleteData(d);
state.SetItemsProcessed(state.iterations());
}
void BM_update_old_L(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_update_old<Size::L>(state);
}
BENCHMARK(BM_update_old_L);
void BM_update_new_L(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_update_new<Size::L>(state);
}
BENCHMARK(BM_update_new_L);
void BM_update_old_XL(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_update_old<Size::XL>(state);
}
BENCHMARK(BM_update_old_XL);
void BM_update_new_XL(benchmark::State& state) {
MujocoErrorTestGuard guard;
BM_update_new<Size::XL>(state);
}
BENCHMARK(BM_update_new_XL);
} // namespace
} // namespace mujoco