Skip to content

Substeps vs iterations

25 · Convergence

The same 30 Hz frame is split into 2, 4, 8 or 16 steps, keeping 48 PGS sweeps per frame. Smaller steps make each contact's correction smaller and the problem easier to converge, so a tall stack of ten 8 cm boxes that topples at large steps stands at small ones.

0.00 s

Cost

Time per 30 Hz frame, CUDA graph replay, median of 7 interleaved rounds on one NVIDIA RTX PRO 6000 Blackwell Server Edition; ratios are against the first row. Solver allocation is the GPU memory the solver allocates at construction, before warm-up and graph capture. Scene: the 8-box stack in each world; every option simulates the same 1/30 s frame.

Option1 world64 worlds1024 worldssolver allocation, 1024 worldsresult in this tutorial
2 x 243.62 ms4.01 ms5.25 ms215 MB10-box stack falls
4 x 124.23 ms 1.17×5.18 ms 1.29×7.22 ms 1.38×215 MB10-box stack falls
8 x 65.39 ms 1.49×6.81 ms 1.70×9.96 ms 1.90×215 MBstands, 8.7 mm sag
16 x 37.60 ms 2.10×10.57 ms 2.64×14.75 ms 2.81×215 MBstands, 3.8 mm sag

What to look at

  • 2 and 4 substeps: the stack drifts and falls.
  • 8 and 16 substeps: it stands, with less sag at 16.

Key parameter

dt

The step size you pass to solver.step. Substeps cost collision and setup per step; iterations cost only sweeps.

Try changing

  • Try 8 substeps × 12 iterations.
  • Set contact_warmstart=True at 4 substeps.

Concept

Substeps vs iterations

What it is

Halving the step halves each correction and the change per step, which helps convergence more than doubling sweeps at a fixed step. Substeps cost more, though: each step repeats collision, mass-matrix work and integration.

In this scene

At 48 sweeps per 30 Hz frame, the 10-box stack collapses at 2×24 and 4×12 and stands at 8×6 and 16×3.

Considerations

Cost
Per 1/30 s frame on the 8-box stack: 2×24 3.6 ms, 4×12 4.2, 8×6 5.4, 16×3 7.6 in one world; 5.3, 7.2, 10.0 and 14.8 ms in 1024 worlds.
Failure modes
Tall stacks of small boxes at large steps drift sideways until they topple.
Use it when
Substeps for tall stacks and fast contacts; iterations when per-frame cost matters, since they are cheaper per frame (see Cost).

The script

cd tutorials && python 25_substeps.py prints each variant's final metric; --record out.json also writes the replay. Every tutorial imports replay.py from the same folder.

tutorials/25_substeps.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Substeps vs iterations: split the same 30 Hz frame into more, cheaper solver steps."""
import newton
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
FRAME_DT = 1.0 / 30.0
N, H = 10, 0.04
def build():
builder = newton.ModelBuilder()
builder.add_ground_plane(cfg=newton.ModelBuilder.ShapeConfig(mu=0.6))
for i in range(N):
box = builder.add_body(
xform=wp.transform((0.0, 0.0, H + 2 * H * i + 0.0002 * i), wp.quat_identity())
)
builder.add_shape_box(box, hx=H, hy=H, hz=H, cfg=newton.ModelBuilder.ShapeConfig(mu=0.6))
return builder.finalize()
def simulate(rec, substeps, iterations):
model = build()
solver = SolverSteel(model, iterations=iterations)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
dt = FRAME_DT / substeps
run = rec.variant(f"{substeps} substeps x {iterations} iterations", model)
for _ in range(rec.frames(4.0)):
top = state_0.body_q.numpy()[N - 1, 2]
run.frame(state_0, sag_mm=1000.0 * (H * (2 * N - 1) - top))
for _ in range(substeps):
pipeline.collide(state_0, contacts)
solver.step(state_0, state_1, control, contacts, dt)
state_0, state_1 = state_1, state_0
run.check(solver)
if __name__ == "__main__":
rec = Recorder()
simulate(rec, 2, 24) # 48 sweeps per frame
simulate(rec, 4, 12)
simulate(rec, 8, 6)
simulate(rec, 16, 3)
rec.save()

Recorded with SolverSteel on Newton 1.9.0.dev0, Warp 1.19.0, NVIDIA RTX PRO 6000 Blackwell Server Edition. Replays read the state back every frame, so they are not a speed benchmark. Script · page text