Determinism support 1/N#1281
Conversation
70cfe01 to
3678db3
Compare
|
@mar-yan24 thank you for contributing this feature to mujoco warp! |
|
@mar-yan24 fyi there is a warp draft pr for introducing determinism in warp NVIDIA/warp#1355 |
|
We just discussed this - it could be worth pursuing this approach in parallel to Warp's low-level support for determinism as they two different approaches may have different performance tradeoffs. |
|
@thowell, thanks for the input! Actually I haven't kept up with Warp as closely recently so I'll take a look at the PR brought up there and see if there are similar ideas compared to what I have in my current plan. Regarding @erikfrey's comment, I don't mind working on the rest of the determinism implementation for this PR and comparing the performance once finished. I'll probably continue working on this for the week and I'll try to finish by around a week from now for the full end-to-end implementation. Thank you both for the info/updates! |
|
Thanks for the review @thowell! The changes should be good to go. I am planning the next determinism steps after this PR like constraint row allocation and actuator moment allocation. Before I continue building, would you prefer that I keep extending this branch/PR so the work is all on this PR or split it up into separate requests for review. Either works for me. |
|
@mar-yan24 lets create separate prs for the next deterministic features. thanks! |
|
@mar-yan24 can we benchmark the performance of this pr with the built-in determinism from warp NVIDIA/warp#1355? probably makes sense to confirm that writing custom deterministic kernels is more performant compared to the general purpose warp determinism functionality. thanks! |
|
@thowell I just tried running the NVIDIA/warp#1355 build on my machine and it seems there are several issues with it that currently make it incompatible with mujoco_warp. When I first tried running, it just crashed, so I disabled graph capture. I think the Warp PR crashes inside The PR's codegen looks up the destination array for each I don't mind helping try and fix this/look into this deeper, but I suspect there may be other blockers as well. In the meantime, I can draft a minimal repro kernel for the PR? Let me know your thoughts. |
|
hello guys, |
|
Hey @johnnynunez I took a bit of a break on some of the projects I was working on as I started a new job recently. Alongside this, I am also waiting in part to take a look at the final implementation of determinism on the Warp side before doing too much more as I want to wait and see what the reach of the implementation there will be. If you would like to contribute and help out in what I have here, be my guest. But I might take another week or two before I have enough time to pick things back up :) |
thank you... let me push from Nvidia side (warp) |
|
Following up here after working through the warp side (NVIDIA/warp#1355, fix PR mmacklin/warp#3) — continuing this work as @mar-yan24 invited contributors. Rebased branch available: I rebased this PR onto current main (96 commits forward, conflicts in Benchmark: custom deterministic kernels vs warp automatic determinism (@thowell asked about this). 20-body contact-rich pile, CUDA graph captured, 1000 steps:
The custom-kernel approach in this PR costs ~9-12%; warp's automatic interception costs 3x at 1 world and collapses at high world counts (the per-launch sort-reduce dominates, and scatter buffers hit the int32 allocation limit at 1024 worlds × njmax=2048). This matches @eric-heiden's observation on the warp PR that automatic mode is too slow for MuJoCo and custom kernels are the right path — i.e., this PR's approach is the right architecture. Determinism measurements on the rebased branch (same-process, bit-identical full-Data resets, 200 steps): contact/constraint assembly is bitwise stable, and single-step replay is fully bitwise. Long rollouts still split at ~step 115 (also ~108 on main with and without any determinism flags; first divergent arrays are solver-internal |
|
Root cause of the residual long-rollout drift found — correcting my earlier guess (it is not mathdx/blocked-Cholesky; it's two racing float atomics in the sparse solver path). Method: bit-identical full-Data resets in one process, replaying 200-500 steps, replacing solver sub-phases one at a time with order-deterministic equivalents and checking the first divergent step (~108 on main for a 20-body pile). Eliminations (each alone does NOT fix it): serial sparse LDL factorization, serial sparse LDL backsubstitution, forced full H rebuild, tolerance=0, CG vs Newton (CG drifts identically → Cholesky/mathdx exonerated), warp deterministic_max_records, zeroed workspaces. The minimal fixing pair (drift gone over 500 steps, 3/3 runs):
A third, smaller source remains in linesearch-adjacent atomics ( This cleanly defines determinism 3/N scope: deterministic sparse |
|
Follow-up to the root-cause analysis above: the fix is now implemented as determinism 3/N — mar-yan24#8. End result across the stack (1/N rebase + 2/N port + 3/N): bitwise |
|
@mar-yan24 could you update your fork and all branch with main? then my PRs will be clean |
|
Wow @johnnynunez! Your development speed is quite fast; I cannot keep up. Are you still looking for me to update my branches? They are somewhat deprecated, so I need to take a look at the new conflicts. I see your new PRs though, thanks for the contribution! This weekend I will look into your new PRs. In the meantime, I can rebase my code. |
Add opt.deterministic flag with post-narrowphase contact sort (#562)
I was previously working on differentiation support for MJWarp but I am taking a break from that because the contacts are giving me a hard time. I can't seem to figure out how to optimize the gradient landscape while keeping good dynamics from rigid contact and coulombic friction. Thus, I have decided spending some time on this would be of more use for now lol.
Summary
This is one of several phased additions. This is a basic PR that just adds an opt-in
opt.deterministicflag that sorts contacts after narrowphase by(worldid, geom0, geom1, geomcollisionid)usingwp.utils.radix_sort_pairs. This fixes the most upstream source of run-to-run non-determinism on GPU: contact index permutation fromatomic_addcounters in narrowphase and CCD. After sorting,d.contact.*is rewritten in canonical order before any downstream kernel reads it.Downstream state (
qacc,qvel,qpos, constraint force, solver reductions) is not yet bitwise reproducible. Follow-ups needed, see Roadmap below.Changes
types.py:Option.deterministic: bool(defaultFalse). Docstring notes phase 1 scope and ~5-10% overhead.io.py: Wires the default input_model, adds the field tooverride_modelsoopt.deterministic=Trueworks from the CLI.collision_driver.py:_sort_contacts()runs after_narrowphase()when the flag is set. Composite 32-bit key:((world * ngeom + geom0) * ngeom + geom1) * gcid_max + gcid. Falls back togcid_max = 1on int32 overflow. Three gather-permute kernels rewrited.contact.*from temp buffers.determinism_test.py: 8 parameterized tests -> contact ordering, field bitwise equality across repeat runs, sort key monotonicity, default-false smoke check.Test results
8/8 pass on RTX 4060 Laptop (sm_89, Ada Lovelace), Warp 1.13.0.dev20260302:
Coverage: contact geom arrays bitwise identical across 3 runs x 10 steps at two
nworldsizes. All contact fields (dist, pos, frame, dim, worldid, geomcollisionid) bitwise identical. Sort key monotonicity verified. DefaultFalseconfirmed (no cost unless opted in).Benchmarks
I had claude help me formulate some benchmarks to see the potential overhead with this implementation. 3 trials x 500 steps, 50-step warmup,
wp.synchronize()fences around the timing window.Newton + Dense, RTX 4060 Laptop (sm_89)
CG + Sparse, RTX 4060 Laptop (sm_89)
All configs under 25% overhead. Worst case is +17.7% (humanoid nworld=512, Newton+Dense); actually one trial in that config hit +28.9% but had 208 ms stdev vs ~65 ms for adjacent configs. Im pretty sure that is likely thermal throttling on my crappy laptop lol.
Overhead % is roughly flat across
nworldwithin each solver path. The bottleneck is the 17wp.empty_likecalls in_sort_contacts, not the GPU sort itself. I am planning on implementing pre-allocated scratch buffers and will fix this in a follow-up, let me know thoughts.Roadmap
Full reproducibility obviously needs more phases:
My rough plan at the moment is to work on constraint row allocation next, this is probably what will help open up downstream effects. After that I will work on actuator moment allocation. Both of these will be done using prefix-sum.
The biggest fix later will be implementing solver reductions, i.e. cost, grad_dot, search_dot. This should make
d.qaccbitwise stable and thus follows qpos and qvel as well.This current PR does not make simulation bitwise reproducible end to end. It guarantees only that
d.contact.*is stable across runs of the same input. End to end full state reproducibility will probably come after some more phases are released.