Generate CUDA for a robot#
GRiD’s code generator (grid_codegen/ in this repository) produces a
robot-specific grid.cuh header; generating the header does not compile or
run CUDA. The grid-generate CLI ships in the grid-rbd wheel, so
pip install grid-rbd (installation)
is enough; only a source checkout also needs its Git submodules
populated.
Generate a header#
From the repository root (the sample URDFs under grid_assets/robots/ are
in the checkout, not the wheel; substitute your own URDF path):
grid-generate grid_assets/robots/iiwa14.urdf --algorithm-list dynamics -o /tmp/iiwa_grid.cuh
--algorithm-list restricts emission to named algorithms or profiles
(next section). Omit it to use the full profile. Add -f for a floating
base; use --no-mujoco-kernels to omit the additional MuJoCo-convention
kernels. For collision geometry, see Collision code generation.
Profiles#
--algorithm-list (and algorithm_list= / codegen_profile= in
Python) accepts algorithm keys (underscored, e.g. inverse_dynamics) and
profile names (hyphenated). A profile is a curated set; whatever you name is
expanded with its dependencies before emission. The profiles, from
grid_codegen/_algo_profiles.py:
Profile |
Selects |
|---|---|
|
every default algorithm: dynamics, gradients, second order, kinematics,
integrators, regressors, centroidal and energy terms (the opt-in keys
|
|
inverse / forward dynamics, |
|
|
|
the core three plus |
|
the core three, both first-order gradients, |
|
the core three, both first-order gradients, |
|
|
|
|
|
|
|
|
|
|
|
the core three, |
|
|
The grid_plant block (costs, barriers, the plant step and the cost menu)
rides on the request: it is emitted for all, for any request that carries
a plant algorithm (the integrators, fdsva_so, the end-effector pose
derivatives, com, ccrba), baked cost or contact frames, or a
multi-target batch, and whenever --plant (emit_plant=True in Python)
is given. kinematics and dynamics-core headers carry no plant block
unless asked.
Fewer kernels in the header means a smaller header, a faster nvcc and
less RAM; see Compile time and footprint below.
Generate from Python#
from URDFParser import URDFParser
from grid_codegen import GRiDCodeGenerator
robot = URDFParser().parse("grid_assets/robots/iiwa14.urdf")
generator = GRiDCodeGenerator(robot)
generator.gen_all_code(
algorithm_list=["dynamics"],
output_path="/tmp/iiwa_grid.cuh",
)
GRiDCodeGenerator is the Python class name, not a separate repository
or installable module. GRiD’s code generator vendors GLASS device-side
linear algebra into the header by default. Set vendor_glass=False when
using an external glass.cuh on your C++ include path.
Use the generated code#
For Python applications, Python Wrappers (grid-rbd) handles generation, compilation and caching through
grid_rbd.load_robot(...).For CUDA applications, include the header in your own translation unit (next section). See GRiD’s code generator for the entry-point layers and Library-safe initialization and cleanup for allocation and cleanup.
Use Fast Robot Setup to reduce large-robot build time and memory, and CUDA Validation And Performance Reporting to check generated results.
Include it in your kernel#
This is the entry point for CUDA applications: the header is self-contained (GLASS
device-side linear algebra is vendored in by default), so a kernel that
#includes it needs only nvcc. Every algorithm has a _device
routine that runs inside your block on shared-memory operands, which is how
you fuse GRiD dynamics into an MPC, RL or controls kernel:
#include "grid.cuh" // generated: grid-generate robot.urdf -o grid.cuh
template <typename T>
__global__ void my_kernel(T *d_tau, const T *d_q, const T *d_qd, const T *d_qdd,
const grid::robotModel<T> *model, T gravity) {
__shared__ T s_q[grid::NUM_JOINTS], s_qd[grid::NUM_JOINTS],
s_qdd[grid::NUM_JOINTS], s_tau[grid::NUM_JOINTS];
for (int i = threadIdx.x; i < grid::NUM_JOINTS; i += blockDim.x) {
s_q[i] = d_q[i]; s_qd[i] = d_qd[i]; s_qdd[i] = d_qdd[i];
}
__syncthreads();
grid::inverse_dynamics_device<T>(s_tau, s_q, s_qd, s_qdd, model,
/*d_f_ext=*/nullptr, gravity);
__syncthreads(); // fuse your own math on s_tau here
for (int i = threadIdx.x; i < grid::NUM_JOINTS; i += blockDim.x) d_tau[i] = s_tau[i];
}
// host: see "Initialising the header" below for the two init spellings;
// auto *model = grid::init_robotModel<float>();
// my_kernel<float><<<1, 128, grid::INVERSE_DYNAMICS_DEVICE_DYNAMIC_SHARED_MEM_BYTES<float>()>>>(...);
nvcc -arch=sm_90 -std=c++17 my_kernel.cu -o my_kernel # pick your GPU's arch
Initialising the header: aggregate or per algorithm#
Before the first launch the generated kernels’ dynamic shared-memory attributes must be registered and the host streams created. The header offers two spellings of that:
// (a) aggregate — simplest. Registers EVERY kernel in the header, so the
// translation unit also compiles every one of them.
cudaStream_t *streams = grid::init_grid<float>();
// (b) per algorithm — register only what you launch; what the test
// runners and the grid_rbd bindings use.
cudaStream_t *streams = grid::init_grid_streams<float>();
gpuErrchk(grid::init_grid_kernel_attr_inverse_dynamics_checked<float>());
// one init_grid_kernel_attr_<algo>_checked<T>() per algorithm you launch
init_grid<T>() is the shortest path when the header was
generated with a trimmed --algorithm-list, because then “every kernel”
is the handful you asked for. With a full-profile header it is what makes a
small .cu file take minutes to compile: a translation unit compiles every
kernel it takes the address of, so compile time and peak RAM scale with the
number of kernels in the header, not with the number you call. The
per-algorithm twins init_grid_streams<T>() +
init_grid_kernel_attr_<algo>_checked<T>() (one per algorithm key, e.g.
..._attr_forward_dynamics_checked, ..._attr_minv_checked; grep your
header for init_grid_kernel_attr_) register only what you name, so the
rest of the header stays uncompiled. The _checked form returns
cudaError_t and never exits; Library-safe initialization and cleanup
has the full contract and the matching teardown.
Compile time and footprint#
Three levers decide how long nvcc takes on a generated header and how
much RAM it needs:
Emit less:
--algorithm-list/algorithm_list=with the profiles above (and--no-mujoco-kernelson a floating base, where the MuJoCo-convention twins roughly double a build). The header only contains what you name plus dependencies.Register less: with a full header, the per-algorithm init twins above compile only the kernels you launch.
Fit the device: the resource tiers trade shared memory for global workspace per kernel; the tier you instantiate is the one that gets compiled.
Measured cold-build times, RAM and the warm path for the Python wrapper’s
.so (which compiles the same header) are tabulated on
Fast Robot Setup.
examples/cuda/ holds the full version of this kernel (the easy _device
call and the full-control _inner call with explicit workspace), plus
build_and_validate.sh, which generates the header, builds, runs and checks
the output against the reference implementation; the notebook
examples/notebooks/07_inline_cuda.ipynb is the same walk-through run
inline. Each _device routine’s dynamic shared-memory requirement is
exposed as grid::<ALGO>_DEVICE_DYNAMIC_SHARED_MEM_BYTES<T>(); the
resource tiers page covers fitting a
routine into a tight outer kernel.