Skip to content

Iterations and Nesterov momentum

23 · Convergence

A stack of eight 10 cm boxes. Each Gauss-Seidel sweep carries load only a short way down the stack, so the bottom of a tall stack converges last and the top sags when there are few iterations. Nesterov momentum with restart speeds up exactly this slow, low-frequency error.

0.00 s

Cost

Time per step, 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, 8 steps of 1/240 s per frame.

Option1 world64 worlds1024 worldssolver allocation, 1024 worldsresult in this tutorial
iterations=60.67 ms0.85 ms1.24 ms215 MBsag 4.6 mm
iterations=6 + Nesterov0.73 ms 1.09×0.91 ms 1.07×1.30 ms 1.05×219 MBsag 3.0 mm
iterations=121.01 ms 1.51×1.21 ms 1.42×1.65 ms 1.33×215 MBsag 2.1 mm
iterations=12 + Nesterov1.10 ms 1.64×1.30 ms 1.53×1.88 ms 1.51×219 MBsag 0.63 mm
iterations=241.67 ms 2.48×1.87 ms 2.20×2.53 ms 2.04×215 MB

What to look at

  • 6 iterations: the top sags about 4.6 mm, and 3 mm with Nesterov.
  • 12 iterations: about 2 mm; with Nesterov about 0.6 mm.

Key parameter

iterations / nesterov_momentum

PGS iterations per step (default 12). nesterov_momentum adds accelerated sweeps with restart (default off).

Try changing

  • Stack 16 boxes.
  • Try iterations=24 without momentum.

Concept

Nesterov momentum

What it is

Gauss-Seidel removes high-frequency error quickly and low-frequency error slowly; a tall stack's load path is the slow kind. Nesterov momentum starts each sweep from impulses extrapolated along the previous change, and restarts the momentum whenever a sweep's correction opposes the extrapolation (O'Donoghue-Candès restart), as MuJoCo's PGS does.

y = λₖ + βₖ (λₖ − λₖ₋₁), βₖ = (k−1)/(k+2)

In this scene

Top sag falls from 4.6 to 3.0 mm at 6 iterations and from 2.1 to 0.63 mm at 12.

Considerations

Cost
+5 to 13% per step (12 iterations, 1024 worlds: 1.65 → 1.88 ms) and 8 bytes per row of history (4.5 MB at 1024 worlds).
Interactions
Requires serial solve, immediate response and interleaved rows; bypasses the specialized sparse and local solves. CUDA only.
Use it when
Stacks, mass ratios and long chains at 8+ iterations; gains are small at very low counts.

The script

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

tutorials/23_iterations_nesterov.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Iterations and Nesterov momentum on a tall stack."""
import newton
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
N, H = 8, 0.05 # eight 10 cm boxes
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, label, **options):
model = build()
solver = SolverSteel(model, **options)
pipeline = newton.CollisionPipeline(model)
contacts = pipeline.contacts()
state_0, state_1, control = model.state(), model.state(), model.control()
run = rec.variant(label, 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, "iterations=6", iterations=6)
simulate(rec, "iterations=6, nesterov_momentum=True", iterations=6, nesterov_momentum=True)
simulate(rec, "iterations=12 (default)")
simulate(rec, "iterations=12, nesterov_momentum=True", nesterov_momentum=True)
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