Skip to content

Gather applied forces per dof in deterministic steps - #1937

Closed
johnnynunez wants to merge 4 commits into
google-deepmind:mainfrom
johnnynunez:feature/determinism-atomics-applied-forces
Closed

johnnynunez wants to merge 4 commits into
google-deepmind:mainfrom
johnnynunez:feature/determinism-atomics-applied-forces

Conversation

@johnnynunez

@johnnynunez johnnynunez commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

With opt.deterministic=ATOMICS, _apply_ft_scatter (#1891) runs its float atomic in Warp's RUN_TO_RUN mode, which records and sorts every contribution. Deterministic steps now gather instead: a thread per body sets the bit of each body with a wrench (_apply_ft_mask) and a thread per dof adds the flagged bodies of its subtree in body order (_apply_ft_gather_mask). Dense wrenches keep _apply_ft_gather. _apply_ft_scatter keeps no deterministic variant, like _solve_LD_sparse_fused, and its default kernel generates the same CUDA source as before.

mjwarp-testspeed with ATOMICS at 8192 worlds, median of 3, on top of #1932 and #1933: humanoid goes from 4.42M to 9.53M steps/s (NONE 9.59M) and three humanoids from 0.55M to 2.08M (NONE 2.14M). Mask and gather take 7.3 + 2.0 us of GPU time per step on three humanoids, against 7.4 us for the default scatter.

The determinism tests check that deterministic steps do not launch _apply_ft_scatter.

cc @thowell @adenzler-nvidia

With `opt.deterministic=ATOMICS`, `_apply_ft_scatter` ran its float
atomic in Warp's RUN_TO_RUN mode, which records and sorts every
contribution on each step.

`apply_ft` already has a kernel without atomics for dense wrenches,
`_apply_ft_gather`: one thread per dof adds the bodies of its subtree
in order. Deterministic steps now use it for sparse wrenches too, so
`_apply_ft_scatter` needs no deterministic variant. Its default kernel
generates the same CUDA source as before.

mjwarp-testspeed with ATOMICS and 8192 worlds, median of 3, RTX PRO
6000, on top of google-deepmind#1932, google-deepmind#1933 and google-deepmind#1915 (steps per second):

  humanoid          4.33M -> 9.11M   (NONE 9.24M)
  three humanoids   0.54M -> 2.01M   (NONE 2.14M)

The gather takes 14.0 us per step on three humanoids and 7.3 us on
humanoid, against 7.4 and 3.5 us for the default scatter.

The determinism tests now check that deterministic steps do not launch
`_apply_ft_scatter`, and `test_apply_ft_subtrees` also runs with
ATOMICS.
Deterministic steps map the applied wrenches to dof space with
_apply_ft_gather: a thread per dof read the wrench of every body of its
subtree, twice the cost of the default scatter when no body has one.

A thread per body now sets the bit of each body with a wrench with an
integer or (_apply_ft_mask, the same loads as a scatter thread that
returns), plus one word per world that tells whether it has any. The
gather (_apply_ft_gather_mask) reads that word and returns for a world
without wrenches; otherwise it adds the bodies of its subtree whose bit
is set, in body order: the same sum as before. Dense wrenches (fluid
forces) keep _apply_ft_gather, and the default scatter is not edited.

8192 worlds, us of GPU time per step (default scatter / deterministic
before / after = mask + gather):
  humanoid           3.5 /  7.3 / 3.5 + 1.1
  unitree_g1_flat    5.5 / 12.4 / 5.4 + 1.2
  three_humanoids    7.4 / 13.9 / 7.6 + 2.0
The mask is a zeroed array of 2 + (nbody - 1) / 31 ints per world.

Measured and rejected: a thread per world or per word that tests its
bodies one after the other (15 to 29 us), and several dofs per gather
thread (1.5 -> 2.3 us with 4).
@johnnynunez

Copy link
Copy Markdown
Contributor Author

Benchmarks on main 60dbbdf: mjwarp-testspeed, benchmarks/ settings (8192 worlds), opt.deterministic=ATOMICS, steps/s, median of 3 interleaved runs (spread under 1%), zero overflow, RTX PRO 6000 Blackwell. "Stack" is main with #1931, #1932, #1933, #1935, #1936, #1937 and #1940.

benchmark main this PR stack without this PR stack NONE on main
humanoid 1.79M 2.27M 4.42M 9.55M 9.58M
three_humanoids fails fails 547k 2.08M 2.14M

"fails": Warp's scatter buffer for the solver kernels exceeds int32 (#1933). With NONE this PR is within 0.1% of main on both benchmarks.

@thowell

thowell commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

@johnnynunez plan for now is to utilize warp's built-in deterministic atomics functionality

@thowell thowell closed this Nov 12, 2026
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