Skip to content

Scale the noslip cost-change guard to float32 rounding - #1964

Merged
adenzler-nvidia merged 1 commit into
google-deepmind:mainfrom
adenzler-nvidia:adenzler/noslip-guard-rounding
Nov 13, 2026
Merged

adenzler-nvidia merged 1 commit into
google-deepmind:mainfrom
adenzler-nvidia:adenzler/noslip-guard-rounding

Conversation

@adenzler-nvidia

Copy link
Copy Markdown
Collaborator

The noslip solver restores a contact's old friction forces when an update raises the cost by more than 1e-10, which is MuJoCo's double-precision threshold. In float32 that is below the rounding of the new forces: on an active cone row the gradient is not zero, so one ulp of force moves the cost by about eps * |force * gradient|, a few 1e-7 in the test below. Near convergence the guard therefore rejects valid updates depending on rounding, and because a rejected update leaves the state unchanged, every later sweep rejects it again and the contact stalls short of MuJoCo's solution. This showed up as a CPU-only failure of test_noslip43 in #1879, which only changes the main solver's rounding.

This adds that rounding bound to the threshold in the pyramidal and elliptic guards, with the same 8 * eps factor the line search uses. Evaluating the cost change in float64 does not help, because the noise is in the rounded forces and not in the arithmetic. A failed QCQP returns zero force, for which the bound is zero, so those updates are still restored as before.

The new test sweeps the spin of a sphere on a plane (elliptic, condim 6) over 61 worlds; on main some of these worlds miss MuJoCo's qacc by up to 1.9e-4 relative. In the pyramidal guard the rejected updates I found were only 1-2 ulps, so that change is for consistency and has no test of its own.

humanoid at 8192 worlds on an RTX PRO 6000 Blackwell, main → this PR in steps/s: 2.009M → 2.010M with noslip_iterations=3, 1.919M → 1.918M with elliptic cones as well, 9.128M → 9.125M with noslip off.

The noslip solver restores a contact's old friction forces when an update raises the cost by more than 1e-10, MuJoCo's double-precision threshold. In float32 the cost change of a nearly converged update is below the rounding of the new forces: on an active cone row the gradient is not zero, so one ulp of force moves the cost by about eps * |force * gradient|. The guard then rejects valid updates depending on rounding, and because a rejected update leaves the state unchanged, every later sweep rejects it again and the contact stalls short of MuJoCo's solution.

Add that rounding bound to the threshold in the pyramidal and elliptic guards, so only a cost increase above float32 noise restores the old forces.

@thowell thowell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks @adenzler-nvidia -- nice catch and clean fix scaling the noslip cost-change guard to float32 force-rounding noise while preserving zero-force rollback on failed QCQPs.

@adenzler-nvidia
adenzler-nvidia merged commit df35beb into google-deepmind:main Nov 13, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants