Skip to content

Row capacity and overflow

56 · Solver and runtime

Constraint rows are stored per world with fixed capacities. row_watermark=True records the most rows each family needed, so you can size free_body_constraint_max and articulation_constraint_max. Rows beyond the capacity are dropped, flagged in solver.constraint_overflow, and reported by check_constraint_capacity().

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
free_body_constraint_max=1280.84 ms1.02 ms1.31 ms152 MBdrops rows on this stack
free_body_constraint_max=512 (default)1.01 ms 1.21×1.21 ms 1.18×1.65 ms 1.26×215 MBfits
free_body_constraint_max=20481.02 ms 1.22×1.21 ms 1.18×1.68 ms 1.29×466 MBfits

What to look at

  • The nine boxes need about 310 free-body rows at peak, which the default 512 fits.
  • With 200 rows, contacts are dropped as the boxes land and check_constraint_capacity() raises; here the boxes sink only a few millimetres, but in a larger scene objects fall through.

Key parameter

row_watermark / check_constraint_capacity()

Read constraint_row_watermarks() after a representative run and set the capacity with some margin.

Try changing

  • Find the smallest capacity with no overflow.
  • Read solver.constraint_overflow in a kernel as an RL termination signal.

Concept

Row capacity

What it is

Rows live in fixed per-world buffers: 32 rows for articulations and 512 for free bodies by default. Rows that do not fit are dropped and the world is flagged in constraint_overflow, which a kernel can read during a captured run.

In this scene

Nine falling boxes need about 310 free-body rows; a capacity of 200 drops contacts as they land.

Considerations

Cost
Memory grows with capacity × worlds: for 1024 eight-box worlds the solver allocates 152 MB at 128 rows, 215 MB at the default 512 and 467 MB at 2048; time barely changes.
Failure modes
Dropped contacts let objects sink or fall through, sometimes only for a frame. Which contacts keep their rows follows the order of reservation, which by default changes every step; a contact that penetrated while dropped is pushed out when it gets rows back, which adds energy and can make the state diverge. With deterministic=RUN_TO_RUN and CollisionPipeline(deterministic=True) the selection repeats for the same inputs; in a measured folding-chain scene the kept set changed much less between steps and repeated runs matched bitwise. That helps reproduce an overflow but does not fix it.
Use it when
Size from row_watermark over a representative episode with margin, so that nothing overflows; check check_constraint_capacity in tests.

The script

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

tutorials/56_row_capacity.py
# SPDX-FileCopyrightText: Copyright (c) 2026 Skild AI and the Steel contributors
# SPDX-License-Identifier: Apache-2.0
"""Row capacities: measure with row_watermark, catch overflow with check_constraint_capacity."""
import newton
import warp as wp
from newton.solvers import SolverSteel
from replay import Recorder
DT = 1.0 / 240.0
SUBSTEPS = 8
def build():
builder = newton.ModelBuilder()
cfg = newton.ModelBuilder.ShapeConfig(mu=0.6)
builder.add_ground_plane(cfg=cfg)
for i in range(3):
for j in range(3):
box = builder.add_body(
xform=wp.transform(
(0.12 * i - 0.12, 0.12 * j - 0.12, 0.3 + 0.03 * ((i + j) % 3)),
wp.quat_rpy(0.1 * i, 0.1 * j, 0.0),
)
)
builder.add_shape_box(box, hx=0.05, hy=0.05, hz=0.05, cfg=cfg)
return builder.finalize()
def simulate(rec, label, **options):
model = build()
solver = SolverSteel(model, row_watermark=True, **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(2.5)):
marks = solver.constraint_row_watermarks()
run.frame(
state_0,
rows_high_water=marks["free_body_raw_high_water"],
lowest_z=state_0.body_q.numpy()[:, 2].min(),
)
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)
try:
solver.check_constraint_capacity()
print(f"{label}: fits, peak {marks['free_body_raw_high_water']} free-body rows")
except RuntimeError as error:
print(f"{label}: {error}")
if __name__ == "__main__":
rec = Recorder()
simulate(rec, "free_body_constraint_max=512 (default)")
simulate(rec, "free_body_constraint_max=200: rows dropped", free_body_constraint_max=200)
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