Resource-Tier System (v2.0)#
Status: shipped in v2.0 + Phase 3a/b/c/d/e spill machinery (L2 pinning
is default-OFF since 2026-09-15 — measured; see the spilled-memory section
below). Inner-controlled placement refactor (below) implemented and
numerically validated for fdsva_so/Minv/FD/ABA/EE_GRAD. Per-tier surgical
spill now also lands for idsva_so (body + world frame) and the
time-integrator value + gradient kernels — see
Resource-Tier Changelog. The global scratch arena is named d_workspace (device memory);
earlier revisions of this doc called it d_global_temp.
Audience: inline-CUDA users (#include "grid.cuh" from their own
kernel). The Python wrappers (grid_rbd.RobotHandle,
grid_rbd.jax.JaxRobotHandle) launch each kernel at its PER-ALGO BAKED
tier — grid::launch_cfg<GRID_ALGO_*>::TIER, autotuned into
config/launch_configs/<robot>/<gpu>.json and baked at codegen. On a
tuned robot many algos run at TIER_LITE/TIER_MINIMAL; an untuned
robot (or algo) falls back to TIER_SHARED.
Note
This page is the REFERENCE (definition, memory model, exposed constants, coverage). The rationale essays live in Resource-Tier Design Notes and the shipped-work log in Resource-Tier Changelog (split from this page 2026-09-09; it used to bury the definition 400 lines deep).
What the tier system is#
Every emitted __global__ kernel and every inline-callable
_device/_inner function takes a non-type template parameter
int RESOURCE_TIER (defaulting to TIER_SHARED). The tier picks a
(launch_bounds, smem footprint, register cap) profile so an
inline-CUDA caller can fit a GRiD primitive into their outer kernel’s
resource budget.
The three tiers:
Tier |
|
Register cap (sm_120) |
Smem behavior |
|---|---|---|---|
|
|
~128-186 regs/thread |
Full inner scratch lives in shared memory; current best perf. |
|
|
~85 regs/thread |
Picks the lowest spill rung that fits the ~48 KB LITE smem target
( |
|
|
~64 regs/thread |
Inner scratch routes entirely to |
The register cap follows from regs_per_thread * max_threads <=
65536 on sm_120: a tighter launch_bounds lets nvcc allocate
more registers per thread, a looser one forces it to budget for more
threads and use fewer registers each.
Who this is for (read this first)#
GRiD is, at its core, a code generator for power users — people who want to call hand-tuned, robot-specialized rigid-body-dynamics kernels directly from their own CUDA code and squeeze every cycle and byte out of the GPU. Everything below the convenience layer is built for that person.
But you do not have to be that person to use GRiD. We deliberately ship a ladder of entry points, from “one line, no GPU knowledge required” up to “hand me the raw block-parallel device routine and I’ll manage the shared memory myself.” Pick the rung that matches how much control you need:
You want… |
Use… |
You manage… |
|---|---|---|
Just the answer, from Python |
|
Nothing. Arrays in, arrays out. Tier comes from the per-algo baked
launch config ( |
The answer, from C++/CUDA host code |
|
Nothing on-device. The wrapper does H2D/D2H copies, picks launch dims, sets shared-mem attributes, launches the kernel. |
A kernel to drop into your own launch |
|
The launch (grid/block dims, dynamic-smem bytes, streams) and the per-trajectory batch loop is done for you inside. |
A block-parallel routine to call inside your own kernel |
|
Everything: shared-memory arenas, scratch placement, syncs. This is the real engine; the layers above are conveniences. |
If you are new, start at the top of that table and ignore the rest of this
document — the RobotHandle tutorial is all you need. If you are here to
fight for occupancy inside a fused planning/MPC/learning kernel, read on: the
rest of this page documents the full machinery so you can drive it directly.
Block-wide parallelism in the inners#
The inners are written to saturate the whole thread block, not a fixed lane count. Every parallel region is emitted as a block-stride loop of the form:
for (int i = threadIdx.x + threadIdx.y * blockDim.x;
i < WORK_ITEMS;
i += blockDim.x * blockDim.y) { ... }
This has several deliberate consequences that power users rely on:
Correct at any block size. The same generated routine runs correctly whether you launch it with 32 threads or 1024. Work items are distributed across however many threads the block has; there are no hard-coded lane assumptions and no out-of-bounds writes when
blockDimdoes not divide the work evenly. (Audited: every parallel write in the emitted code is inside a block-stride loop — there are no barethreadIdx-indexed stores.)You choose the occupancy/latency trade. Because the routine adapts to the launch, you can tune block size for your fused kernel’s occupancy without regenerating anything. The tier system’s
__launch_bounds__only bounds the maximum threads (to control the register budget), it does not fix the launch.Maximal parallelism by construction. Each algorithm exposes its natural parallel width (e.g. per-(i,j,k) tensor elements, per-DOF columns, per-body 6×6 blocks) directly as the loop bound, so a large block fills with useful work rather than idling. Where the recursion structure forces seriality (e.g. the BFS sweeps), the parallel regions sit between syncs and still use the full block.
The practical upshot: the inner is the unit of parallelism. You bring the threads; it uses all of them.
Exposed sizing + placement constants (power-user reference)#
For every spillable inner, codegen emits a matched trio so you can allocate correctly and know what codegen chose. Using fdsva_so as the template:
// bytes to reserve in shared memory for this placement
template <typename T, bool SCRATCH_IN_SMEM = true>
constexpr size_t FDSVA_SO_INNER_SMEM_BYTES();
// bytes to reserve in global memory for this placement
template <typename T, bool SCRATCH_IN_SMEM = true>
constexpr size_t FDSVA_SO_INNER_WORKSPACE_BYTES();
// the placement codegen assigned to each tier, for THIS robot
template <int TIER>
constexpr bool FDSVA_SO_SCRATCH_IN_SMEM();
The same trio is emitted for the other converted algorithms, with the placement bool named for the buffer it controls:
MINV_INNER_{SMEM,WORKSPACE}_BYTES<T, F_IN_SMEM>+MINV_F_IN_SMEM<TIER>FD_INNER_{SMEM,WORKSPACE}_BYTES<T, MINV_F_IN_SMEM>+FD_MINV_F_IN_SMEM<TIER>ABA_INNER_{SMEM,WORKSPACE}_BYTES<T, TEMP_IN_SMEM>+ABA_TEMP_IN_SMEM<TIER>EE_GRAD_INNER_{SMEM,WORKSPACE}_BYTES<T, TEMP_IN_SMEM>+EE_GRAD_TEMP_IN_SMEM<TIER>
The *_device inline entry points (inverse_dynamics_gradient / forward_dynamics_gradient / idsva_so / end_effector_pose_hessian) still
expose their sizing as *_DEVICE_INLINE_{SMEM,WORKSPACE}_BYTES<T, TIER>
(keyed on tier rather than a placement bool); they decide placement internally
via the tier_workspace_expr arena helper, and their kernels inline + spill
at the kernel level by design. Converting those kernels to call
placement-deciding inners is a tracked follow-up.
Rule of thumb for an inline call:
Pick a
TIER(or call<ALGO>_..._IN_SMEM<TIER>()to see the placement it resolves to for your robot).Reserve
..._INNER_SMEM_BYTES<T, placement>()in your block’s dynamic shared memory for the primitive’ss_temp.cudaMalloc(once)..._INNER_WORKSPACE_BYTES<T, placement>()per concurrently-resident block ford_workspace(0 when the placement keeps everything in shared).Call
<algo>_inner<T, placement>(..., s_temp, d_workspace, ...).
Testing status of the refactor: all five converted algos
(fdsva_so / Minv / FD / ABA / EE_GRAD) compile clean at all three tiers across
iiwa14 / go2 / h1_2 (fixed) + g1 (floating); the tier-instantiation smoke
passes. Numerical equivalence (cuda_equivalence) and the per-tier perf
sweep are pending a joint testing session — the Minv/FD arena layout changed
(no_F-then-F instead of F-then-no_F), so equivalence is the gating check.
What’s plumbed today#
The tier knob is exposed at the kernel level on every emitted
*_kernel<T, RESOURCE_TIER>. At the inline-CUDA _device /
_inner level, these functions accept RESOURCE_TIER + a
caller-provided T *d_workspace argument:
fdsva_so_inner<T, RESOURCE_TIER>(s_df2, s_idsva_so, s_Minv, s_df_du, s_XImats, s_temp, d_workspace, gravity)— 4*nv³ inner scratch routes betweens_temp(SHARED) andd_workspace(LITE/MINIMAL).forward_dynamics_gradient_device<T, RESOURCE_TIER>(s_df_du, s_q, s_qd, [s_qdd, s_Minv | s_u], d_robotModel, gravity, d_workspace)— whole s_temp arena routes per tier.inverse_dynamics_gradient_device<T, RESOURCE_TIER>(s_dc_du, s_q, s_qd, [s_qdd], d_robotModel, gravity, d_workspace)— whole s_temp arena routes per tier.end_effector_pose_hessian_device<T, RESOURCE_TIER> (s_d2eePos, s_deePos, s_q, d_robotModel, d_workspace)—s_d2eeTempslot (the 2*16*num_ees*n² portion) routes per tier; inner_no_d2 stays in smem at all tiers.idsva_so_device<T, RESOURCE_TIER>(s_idsva_so, s_q, s_qd, s_qdd, d_robotModel, gravity, d_workspace)— codegen-time frame dispatcher:body_frame_innerfor fixed-base,world_frame_innerfor floating-base. Inner temp arena routes per tier.
Sizing constants — call these from host code to allocate the right buffers:
template <typename T, int TIER = TIER_SHARED>
constexpr size_t FDSVA_SO_INNER_SMEM_BYTES(); // bytes for s_temp at TIER
template <typename T, int TIER = TIER_SHARED>
constexpr size_t FDSVA_SO_INNER_WORKSPACE_BYTES(); // bytes for d_workspace at TIER
// Same pattern: FORWARD_DYNAMICS_GRADIENT_DEVICE_INLINE_*, INVERSE_DYNAMICS_GRADIENT_DEVICE_INLINE_*,
// END_EFFECTOR_POSE_HESSIAN_DEVICE_INLINE_*, IDSVA_SO_DEVICE_INLINE_*
At TIER_SHARED the SMEM_BYTES value matches current behavior
(the temp is in shared); at TIER_LITE/TIER_MINIMAL the
SMEM_BYTES value drops (temp moved out) and the WORKSPACE_BYTES
value covers the moved temp.
Inline-CUDA usage example:
__global__ void my_outer_kernel(...) {
extern __shared__ unsigned char s_arena[];
// ... slice s_arena for your own buffers ...
T *s_grid_temp = /* slice for GRiD primitive */;
// At launch site we passed sizeof(s_grid_temp) =
// grid::FDSVA_SO_INNER_SMEM_BYTES<T, grid::TIER_MINIMAL>()
// == 0 at MINIMAL — no smem reserved for GRiD temp.
// Workspace was malloc'd to:
// grid::FDSVA_SO_INNER_WORKSPACE_BYTES<T, grid::TIER_MINIMAL>()
// == 4 * NV^3 * sizeof(T) at MINIMAL.
grid::fdsva_so_inner<T, grid::TIER_MINIMAL>(
s_df2, s_idsva_so, s_Minv, s_df_du, s_XImats,
/* s_temp */ nullptr, // unused at MINIMAL
/* d_workspace */ workspace, // global mem
gravity);
}
References#
P1 baseline matrix:
test/diagnostics/results/tier_baseline_sm120_rtx5090.md— per-(algo, robot) register + smem + spill data driving tier decisions.Smoke test:
test/diagnostics/tier_instantiation_smoke.py— verifies all 9 single-overload kernels compile at all 3 tiers AND 17 static_asserts validate per-tier SMEM/WORKSPACE invariants.Reusable arena helper:
grid_codegen/helpers/_code_generation_helpers.py:504-583(gen_declare_shared_arena,tier_workspace_expr).Existing bench harness:
test/benchmarks/run_multi_version.py(multi-column driver),test/benchmarks/baselines/{grid,pinocchio,frax,mjx}/(per- baseline runners),test/benchmarks/generate_report.py(output renderer that already understandsfrax_cpu/frax_gpucolumns and gracefully renders missing cells as—).