Skip to content

Commit f1bcd4a

Browse files
authored
[MISC] Speed up rigid constraint solver for large number of constraints via per-island rebuild/incremental decision. (Genesis-Embodied-AI#2996)
1 parent d187f6a commit f1bcd4a

3 files changed

Lines changed: 142 additions & 70 deletions

File tree

examples/collision/tower.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,16 @@ def main():
1010
parser.add_argument("-v", "--vis", action="store_true", default=False)
1111
args = parser.parse_args()
1212
object_type = args.object
13-
horizon = 20 if "PYTEST_VERSION" in os.environ else 600
13+
horizon = 20 if "PYTEST_VERSION" in os.environ else 10000
1414

1515
gs.init(backend=gs.cpu, precision="32", performance_mode=True)
1616

1717
scene = gs.Scene(
1818
sim_options=gs.options.SimOptions(
19-
dt=0.02,
20-
substeps=6,
19+
dt=0.0015,
2120
),
2221
rigid_options=gs.options.RigidOptions(
23-
max_collision_pairs=200,
22+
max_collision_pairs=400,
2423
),
2524
viewer_options=gs.options.ViewerOptions(
2625
camera_pos=(20, -20, 20),

genesis/engine/solvers/rigid/constraint/solver.py

Lines changed: 137 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3185,70 +3185,121 @@ def func_hessian_and_cholesky_factor_incremental_sparse_batch(
31853185

31863186

31873187
@qd.func
3188-
def func_cholesky_factor_incremental_per_island_batch(
3188+
def func_rank1_update_island_constraint(
31893189
i_b,
3190+
i_island,
3191+
i_c,
31903192
island_state: array_class.IslandState,
31913193
constraint_state: array_class.ConstraintState,
31923194
rigid_global_info: array_class.RigidGlobalInfo,
31933195
) -> bool:
3194-
"""Per-island analogue of func_hessian_and_cholesky_factor_incremental_sparse_batch.
3196+
"""Apply one constraint's rank-1 update/downdate to its island's Cholesky block of L, in place in nt_H.
31953197
3196-
Each changed constraint lies in a single island; its rank-1 update/downdate touches only that island's block of L
3197-
(stored in nt_H at the island's global DOF rows/cols, factored in place). The update runs over the island's local
3198-
DOF positions ascending (dof_id is ascending, so local order is global-DOF order = the factor's processing order),
3199-
bounded to the per-island skyline envelope (dof_env_start_local). nt_vec is the rank-1 working vector, indexed by
3200-
global DOF; it self-clears as columns are consumed and is zeroed up front so a degenerate break leaves it clean.
3198+
The update runs over the island's local DOF positions ascending (dof_id is ascending, so local order is global-DOF
3199+
order = the factor's processing order), bounded to the per-island skyline envelope (dof_env_start_local). nt_vec is
3200+
the rank-1 working vector, indexed by global DOF; it self-clears as columns are consumed (the caller zeroes the
3201+
island's entries once up front so a degenerate break leaves it clean). Returns whether the downdate went indefinite,
3202+
in which case the caller refactors the island directly.
32013203
"""
32023204
EPS = rigid_global_info.EPS[None]
3203-
n_dofs = constraint_state.nt_H.shape[1]
3204-
n_islands = island_state.n_islands[i_b]
3205+
dof_base = island_state.dof_slices.start[i_island, i_b]
3206+
n = island_state.dof_slices.n[i_island, i_b]
3207+
sign = 1.0 if constraint_state.active[i_c, i_b] else -1.0
3208+
efc_D_sqrt = qd.sqrt(constraint_state.efc_D[i_c, i_b])
32053209

3206-
for d in range(n_dofs):
3207-
constraint_state.nt_vec[d, i_b] = gs.qd_float(0.0)
3210+
for k_ in range(constraint_state.jac_n_dofs[i_c, i_b]):
3211+
gd = constraint_state.jac_dofs_idx[i_c, k_, i_b]
3212+
constraint_state.nt_vec[gd, i_b] = constraint_state.jac[i_c, gd, i_b] * efc_D_sqrt
32083213

32093214
is_degenerated = False
3210-
for idx in range(constraint_state.incr_n_changed[i_b]):
3211-
i_c = constraint_state.incr_changed_idx[idx, i_b]
3212-
i_island = 0
3213-
if n_islands > 1:
3214-
i_island = island_state.constraint_island_idx[i_c, i_b]
3215-
dof_base = island_state.dof_slices.start[i_island, i_b]
3216-
n = island_state.dof_slices.n[i_island, i_b]
3217-
sign = 1.0 if constraint_state.active[i_c, i_b] else -1.0
3218-
efc_D_sqrt = qd.sqrt(constraint_state.efc_D[i_c, i_b])
3215+
for ld in range(n):
3216+
gk = island_state.dof_id[dof_base + ld, i_b]
3217+
vk = constraint_state.nt_vec[gk, i_b]
3218+
if qd.abs(vk) > EPS:
3219+
Lkk = constraint_state.nt_H[i_b, gk, gk]
3220+
tmp = Lkk * Lkk + sign * vk * vk
3221+
if tmp < EPS:
3222+
is_degenerated = True
3223+
break
3224+
r = qd.sqrt(tmp)
3225+
cinv = Lkk / r
3226+
c = r / Lkk
3227+
s = vk / Lkk
3228+
constraint_state.nt_H[i_b, gk, gk] = r
3229+
for jd in range(ld + 1, n):
3230+
if island_state.dof_env_start_local[dof_base + jd, i_b] <= ld:
3231+
gj = island_state.dof_id[dof_base + jd, i_b]
3232+
constraint_state.nt_H[i_b, gj, gk] = (
3233+
constraint_state.nt_H[i_b, gj, gk] + sign * s * constraint_state.nt_vec[gj, i_b]
3234+
) * cinv
3235+
constraint_state.nt_vec[gj, i_b] = (
3236+
c * constraint_state.nt_vec[gj, i_b] - s * constraint_state.nt_H[i_b, gj, gk]
3237+
)
3238+
constraint_state.nt_vec[gk, i_b] = gs.qd_float(0.0)
3239+
return is_degenerated
32193240

3220-
for k_ in range(constraint_state.jac_n_dofs[i_c, i_b]):
3221-
gd = constraint_state.jac_dofs_idx[i_c, k_, i_b]
3222-
constraint_state.nt_vec[gd, i_b] = constraint_state.jac[i_c, gd, i_b] * efc_D_sqrt
32233241

3224-
for ld in range(n):
3225-
gk = island_state.dof_id[dof_base + ld, i_b]
3226-
vk = constraint_state.nt_vec[gk, i_b]
3227-
if qd.abs(vk) > EPS:
3228-
Lkk = constraint_state.nt_H[i_b, gk, gk]
3229-
tmp = Lkk * Lkk + sign * vk * vk
3230-
if tmp < EPS:
3231-
is_degenerated = True
3232-
break
3233-
r = qd.sqrt(tmp)
3234-
cinv = Lkk / r
3235-
c = r / Lkk
3236-
s = vk / Lkk
3237-
constraint_state.nt_H[i_b, gk, gk] = r
3238-
for jd in range(ld + 1, n):
3239-
if island_state.dof_env_start_local[dof_base + jd, i_b] <= ld:
3240-
gj = island_state.dof_id[dof_base + jd, i_b]
3241-
constraint_state.nt_H[i_b, gj, gk] = (
3242-
constraint_state.nt_H[i_b, gj, gk] + sign * s * constraint_state.nt_vec[gj, i_b]
3243-
) * cinv
3244-
constraint_state.nt_vec[gj, i_b] = (
3245-
c * constraint_state.nt_vec[gj, i_b] - s * constraint_state.nt_H[i_b, gj, gk]
3246-
)
3247-
constraint_state.nt_vec[gk, i_b] = gs.qd_float(0.0)
3248-
if is_degenerated:
3249-
break
3242+
@qd.func
3243+
def func_factor_island_incremental_or_direct(
3244+
i_b,
3245+
i_island,
3246+
island_state: array_class.IslandState,
3247+
entities_info: array_class.EntitiesInfo,
3248+
constraint_state: array_class.ConstraintState,
3249+
rigid_global_info: array_class.RigidGlobalInfo,
3250+
static_rigid_sim_config: qd.template(),
3251+
):
3252+
"""Maintain one island's Cholesky factor for the current active set, choosing per island between an incremental
3253+
rank-1 update/downdate and a direct refactor.
3254+
3255+
One rank-1 update sweeps the island's skyline envelope at O(sum_span) (sum_span = total row span = envelope
3256+
nonzeros), so n_changed of them cost O(n_changed * sum_span); a direct refactor factors it at O(sum_span_sq)
3257+
(sum_span_sq = sum of squared row spans). Both costs are read straight off the envelope, so the decision compares
3258+
them directly - incremental while n_changed * sum_span < sum_span_sq - with no scene-tuned constant. The choice must
3259+
be per island, not on the env-wide flip count: the rebuild path refactors every island, so a global decision would
3260+
needlessly refactor quiescent islands whenever flips are spread thin across many of them (e.g. several separated
3261+
piles each toggling a single contact).
3262+
"""
3263+
c_start = island_state.constraint_slices.start[i_island, i_b]
3264+
c_n = island_state.constraint_slices.n[i_island, i_b]
32503265

3251-
return is_degenerated
3266+
n_changed = 0
3267+
for k in range(c_n):
3268+
i_c = island_state.constraint_id[c_start + k, i_b]
3269+
if constraint_state.active[i_c, i_b] ^ constraint_state.prev_active[i_c, i_b]:
3270+
n_changed = n_changed + 1
3271+
3272+
if n_changed > 0:
3273+
dof_base = island_state.dof_slices.start[i_island, i_b]
3274+
n_isl_dofs = island_state.dof_slices.n[i_island, i_b]
3275+
# Estimate both costs from the skyline envelope: one rank-1 update sweeps the envelope at O(sum_span), a direct
3276+
# refactor factors it at O(sum_span_sq). Incremental wins while n_changed * sum_span < sum_span_sq, i.e. while
3277+
# n_changed stays below the flop-weighted effective bandwidth sum_span_sq / sum_span. No scene-tuned constant.
3278+
sum_span = gs.qd_float(0.0)
3279+
sum_span_sq = gs.qd_float(0.0)
3280+
for ld in range(n_isl_dofs):
3281+
row_span = gs.qd_float(ld - island_state.dof_env_start_local[dof_base + ld, i_b])
3282+
sum_span = sum_span + row_span
3283+
sum_span_sq = sum_span_sq + row_span * row_span
3284+
need_rebuild = gs.qd_float(n_changed) * sum_span > sum_span_sq
3285+
if not need_rebuild:
3286+
for ld in range(n_isl_dofs):
3287+
constraint_state.nt_vec[island_state.dof_id[dof_base + ld, i_b], i_b] = gs.qd_float(0.0)
3288+
for k in range(c_n):
3289+
i_c = island_state.constraint_id[c_start + k, i_b]
3290+
if constraint_state.active[i_c, i_b] ^ constraint_state.prev_active[i_c, i_b]:
3291+
if func_rank1_update_island_constraint(
3292+
i_b, i_island, i_c, island_state, constraint_state, rigid_global_info
3293+
):
3294+
need_rebuild = True
3295+
break
3296+
if need_rebuild:
3297+
func_hessian_direct_batch(
3298+
i_b, i_island, island_state, entities_info, constraint_state, rigid_global_info, static_rigid_sim_config
3299+
)
3300+
func_cholesky_factor_direct_batch(
3301+
i_b, i_island, island_state, constraint_state, rigid_global_info, static_rigid_sim_config
3302+
)
32523303

32533304

32543305
@qd.func
@@ -5148,28 +5199,50 @@ def func_solve_iter(
51485199
if qd.static(static_rigid_sim_config.solver_type == gs.constraint_solver.Newton):
51495200
# Within a step jac, M and efc_D are fixed, so H = M + J.T diag(D active) J depends only on the active mask;
51505201
# the linesearch only moves qacc, never H. func_solve_init already seeded the factor (nt_H holds L for the
5151-
# seed's active set, and update_constraint above set prev_active to it), so every iteration including the
5152-
# first is maintained incrementally: if no constraint flipped active the factor is reused as-is; if a few
5153-
# flipped, the skyline factor is updated by a rank-1 update/downdate per changed constraint (whole-env or
5154-
# per-island; much cheaper than reassembling and re-factoring). A degenerate downdate or a large active-set
5155-
# change (> half the constraints) falls back to a direct rebuild, which `need_rebuild` selects through a
5156-
# single call site (it is a large function, and each call site is compiled separately). The dense path uses
5157-
# its own incremental rank-1 update.
5158-
if qd.static(static_rigid_sim_config.sparse_solve):
5202+
# seed's active set, and update_constraint above set prev_active to it), so every iteration is maintained
5203+
# rather than rebuilt: if no constraint flipped active the factor is reused as-is; if a few flipped, the
5204+
# skyline factor is updated by a rank-1 update/downdate per changed constraint; a degenerate downdate or a
5205+
# large active-set change falls back to a direct refactor. The per-island path decides this per island (the
5206+
# refactor is per island, so a global decision would needlessly rebuild quiescent islands); the whole-env
5207+
# sparse path and the dense path decide on the env-wide flip count.
5208+
if qd.static(
5209+
static_rigid_sim_config.sparse_solve
5210+
and static_rigid_sim_config.enable_per_island_solve
5211+
and not static_rigid_sim_config.sparse_envelope
5212+
):
5213+
for i_island in range(island_state.n_islands[i_b]):
5214+
if qd.static(static_rigid_sim_config.use_hibernation):
5215+
if island_state.is_hibernated[i_island, i_b]:
5216+
continue
5217+
func_factor_island_incremental_or_direct(
5218+
i_b,
5219+
i_island,
5220+
island_state,
5221+
entities_info,
5222+
constraint_state,
5223+
rigid_global_info,
5224+
static_rigid_sim_config,
5225+
)
5226+
elif qd.static(static_rigid_sim_config.sparse_solve):
51595227
func_build_changed_constraint_list(i_b, constraint_state=constraint_state)
51605228
n_changed = constraint_state.incr_n_changed[i_b]
51615229
need_rebuild = True
51625230
if n_changed == 0:
51635231
need_rebuild = False
5164-
elif n_changed * 2 <= constraint_state.n_constraints[i_b]:
5165-
if qd.static(static_rigid_sim_config.sparse_envelope):
5232+
elif qd.static(static_rigid_sim_config.sparse_envelope):
5233+
# Same crossover as the per-island path, on the whole-env skyline (nt_H_env_start): incremental
5234+
# beats a refactor while n_changed * sum_span < sum_span_sq (the flop-weighted effective bandwidth).
5235+
n_dofs = constraint_state.nt_H.shape[1]
5236+
sum_span = gs.qd_float(0.0)
5237+
sum_span_sq = gs.qd_float(0.0)
5238+
for p in range(n_dofs):
5239+
row_span = gs.qd_float(p - constraint_state.nt_H_env_start[i_b, p])
5240+
sum_span = sum_span + row_span
5241+
sum_span_sq = sum_span_sq + row_span * row_span
5242+
if gs.qd_float(n_changed) * sum_span <= sum_span_sq:
51665243
need_rebuild = func_hessian_and_cholesky_factor_incremental_sparse_batch(
51675244
i_b, constraint_state, rigid_global_info
51685245
)
5169-
elif qd.static(static_rigid_sim_config.enable_per_island_solve):
5170-
need_rebuild = func_cholesky_factor_incremental_per_island_batch(
5171-
i_b, island_state, constraint_state, rigid_global_info
5172-
)
51735246
if need_rebuild:
51745247
func_hessian_and_cholesky_factor_direct_batch(
51755248
i_b,

tests/test_rigid_physics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,7 +2037,7 @@ def test_many_boxes_dynamics(box_box_detection, gjk_collision, dynamics, show_vi
20372037
),
20382038
show_viewer=show_viewer,
20392039
)
2040-
plane = scene.add_entity(
2040+
scene.add_entity(
20412041
gs.morphs.Plane(),
20422042
)
20432043
for n in range(5**3):
@@ -2056,7 +2056,7 @@ def test_many_boxes_dynamics(box_box_detection, gjk_collision, dynamics, show_vi
20562056
if dynamics:
20572057
for entity in scene.entities[1:]:
20582058
entity.set_dofs_velocity(4.0 * np.random.rand(6))
2059-
num_steps = 750 if dynamics else 150
2059+
num_steps = 800 if dynamics else 150
20602060
for i in range(num_steps):
20612061
scene.step()
20622062
if i > num_steps - 50:

0 commit comments

Comments
 (0)