Skip to content

Commit 4f0314c

Browse files
Parallelize forward kinematics on CUDA (#3395)
Co-authored-by: Dylan Turpin <dturpin@nvidia.com>
1 parent d1fc0d2 commit 4f0314c

11 files changed

Lines changed: 1053 additions & 220 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Parallelize `newton.eval_fk` forward kinematics across the joints of each articulation on CUDA; differentiable models and non-tree articulation topologies retain the serial implementation.

newton/_src/sim/articulation.py

Lines changed: 232 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,158 @@ def invert_3d_rotational_dofs(
233233
return wp.vec3(angles[0], angles[1], s * angles[2]), wp.vec3(velocities[0], velocities[1], s * velocities[2])
234234

235235

236+
@wp.func
237+
def eval_joint_motion(
238+
type: int,
239+
q_start: int,
240+
qd_start: int,
241+
lin_axis_count: int,
242+
ang_axis_count: int,
243+
joint_q: wp.array[float],
244+
joint_qd: wp.array[float],
245+
joint_axis: wp.array[wp.vec3],
246+
):
247+
X_j = wp.transform_identity()
248+
v_j = wp.spatial_vector(wp.vec3(), wp.vec3())
249+
250+
if type == JointType.PRISMATIC:
251+
axis = joint_axis[qd_start]
252+
X_j = wp.transform(axis * joint_q[q_start], wp.quat_identity())
253+
v_j = wp.spatial_vector(axis * joint_qd[qd_start], wp.vec3())
254+
255+
if type == JointType.REVOLUTE:
256+
axis = joint_axis[qd_start]
257+
X_j = wp.transform(wp.vec3(), wp.quat_from_axis_angle(axis, joint_q[q_start]))
258+
v_j = wp.spatial_vector(wp.vec3(), axis * joint_qd[qd_start])
259+
260+
if type == JointType.BALL:
261+
X_j = wp.transform(
262+
wp.vec3(),
263+
wp.quat(joint_q[q_start], joint_q[q_start + 1], joint_q[q_start + 2], joint_q[q_start + 3]),
264+
)
265+
v_j = wp.spatial_vector(
266+
wp.vec3(),
267+
wp.vec3(joint_qd[qd_start], joint_qd[qd_start + 1], joint_qd[qd_start + 2]),
268+
)
269+
270+
if type == JointType.FREE or type == JointType.DISTANCE:
271+
X_j = wp.transform(
272+
wp.vec3(joint_q[q_start], joint_q[q_start + 1], joint_q[q_start + 2]),
273+
wp.quat(
274+
joint_q[q_start + 3],
275+
joint_q[q_start + 4],
276+
joint_q[q_start + 5],
277+
joint_q[q_start + 6],
278+
),
279+
)
280+
v_j = wp.spatial_vector(
281+
wp.vec3(joint_qd[qd_start], joint_qd[qd_start + 1], joint_qd[qd_start + 2]),
282+
wp.vec3(joint_qd[qd_start + 3], joint_qd[qd_start + 4], joint_qd[qd_start + 5]),
283+
)
284+
285+
if type == JointType.D6:
286+
pos = wp.vec3(0.0)
287+
rot = wp.quat_identity()
288+
vel_v = wp.vec3(0.0)
289+
vel_w = wp.vec3(0.0)
290+
291+
# Keep these updates unrolled so joint actions remain differentiable.
292+
if lin_axis_count > 0:
293+
axis = joint_axis[qd_start]
294+
pos += axis * joint_q[q_start]
295+
vel_v += axis * joint_qd[qd_start]
296+
if lin_axis_count > 1:
297+
axis = joint_axis[qd_start + 1]
298+
pos += axis * joint_q[q_start + 1]
299+
vel_v += axis * joint_qd[qd_start + 1]
300+
if lin_axis_count > 2:
301+
axis = joint_axis[qd_start + 2]
302+
pos += axis * joint_q[q_start + 2]
303+
vel_v += axis * joint_qd[qd_start + 2]
304+
305+
iq = q_start + lin_axis_count
306+
iqd = qd_start + lin_axis_count
307+
if ang_axis_count == 1:
308+
axis = joint_axis[iqd]
309+
rot = wp.quat_from_axis_angle(axis, joint_q[iq])
310+
vel_w = joint_qd[iqd] * axis
311+
if ang_axis_count == 2:
312+
rot, vel_w = compute_2d_rotational_dofs(
313+
joint_axis[iqd],
314+
joint_axis[iqd + 1],
315+
joint_q[iq],
316+
joint_q[iq + 1],
317+
joint_qd[iqd],
318+
joint_qd[iqd + 1],
319+
)
320+
if ang_axis_count == 3:
321+
rot, vel_w = compute_3d_rotational_dofs(
322+
joint_axis[iqd],
323+
joint_axis[iqd + 1],
324+
joint_axis[iqd + 2],
325+
joint_q[iq],
326+
joint_q[iq + 1],
327+
joint_q[iq + 2],
328+
joint_qd[iqd],
329+
joint_qd[iqd + 1],
330+
joint_qd[iqd + 2],
331+
)
332+
333+
X_j = wp.transform(pos, rot)
334+
v_j = wp.spatial_vector(vel_v, vel_w)
335+
336+
return X_j, v_j
337+
338+
339+
@wp.func
340+
def eval_joint_child_state(
341+
type: int,
342+
parent: int,
343+
child: int,
344+
X_wp: wp.transform,
345+
X_pj: wp.transform,
346+
X_cj: wp.transform,
347+
X_j: wp.transform,
348+
v_j: wp.spatial_vector,
349+
body_qd: wp.array[wp.spatial_vector],
350+
body_com: wp.array[wp.vec3],
351+
):
352+
X_wpj = X_pj
353+
if parent >= 0:
354+
X_wpj = X_wp * X_pj
355+
356+
X_wcj = X_wpj * X_j
357+
X_wc = X_wcj * wp.transform_inverse(X_cj)
358+
x_child_origin = wp.transform_get_translation(X_wc)
359+
360+
v_parent_origin = wp.vec3()
361+
w_parent = wp.vec3()
362+
if parent >= 0:
363+
v_wp = body_qd[parent]
364+
w_parent = wp.spatial_bottom(v_wp)
365+
v_parent_origin = com_twist_to_point_velocity(v_wp, X_wp, body_com[parent], x_child_origin)
366+
367+
linear_joint_world = wp.transform_vector(X_wpj, wp.spatial_top(v_j))
368+
angular_joint_world = wp.transform_vector(X_wpj, wp.spatial_bottom(v_j))
369+
if type == JointType.FREE or type == JointType.DISTANCE:
370+
# These joint velocities are COM-referenced, while the tree recurrence is origin-referenced.
371+
v_joint_origin = com_twist_to_origin_twist(
372+
wp.spatial_vector(linear_joint_world, angular_joint_world),
373+
X_wc,
374+
body_com[child],
375+
)
376+
linear_joint_origin = wp.spatial_top(v_joint_origin)
377+
else:
378+
child_origin_offset_world = x_child_origin - wp.transform_get_translation(X_wcj)
379+
linear_joint_origin = linear_joint_world + wp.cross(angular_joint_world, child_origin_offset_world)
380+
381+
v_wc_origin = wp.spatial_vector(
382+
v_parent_origin + linear_joint_origin,
383+
w_parent + angular_joint_world,
384+
)
385+
return X_wc, origin_twist_to_com_twist(v_wc_origin, X_wc, body_com[child])
386+
387+
236388
@wp.func
237389
def eval_single_articulation_fk(
238390
joint_start: int,
@@ -278,149 +430,36 @@ def eval_single_articulation_fk(
278430
lin_axis_count = joint_dof_dim[i, 0]
279431
ang_axis_count = joint_dof_dim[i, 1]
280432

281-
X_j = wp.transform_identity()
282-
v_j = wp.spatial_vector(wp.vec3(), wp.vec3())
283-
284-
if type == JointType.PRISMATIC:
285-
axis = joint_axis[qd_start]
286-
287-
q = joint_q[q_start]
288-
qd = joint_qd[qd_start]
289-
290-
X_j = wp.transform(axis * q, wp.quat_identity())
291-
v_j = wp.spatial_vector(axis * qd, wp.vec3())
292-
293-
if type == JointType.REVOLUTE:
294-
axis = joint_axis[qd_start]
295-
296-
q = joint_q[q_start]
297-
qd = joint_qd[qd_start]
298-
299-
X_j = wp.transform(wp.vec3(), wp.quat_from_axis_angle(axis, q))
300-
v_j = wp.spatial_vector(wp.vec3(), axis * qd)
301-
302-
if type == JointType.BALL:
303-
r = wp.quat(joint_q[q_start + 0], joint_q[q_start + 1], joint_q[q_start + 2], joint_q[q_start + 3])
304-
305-
w = wp.vec3(joint_qd[qd_start + 0], joint_qd[qd_start + 1], joint_qd[qd_start + 2])
306-
307-
X_j = wp.transform(wp.vec3(), r)
308-
v_j = wp.spatial_vector(wp.vec3(), w)
309-
310-
if type == JointType.FREE or type == JointType.DISTANCE:
311-
t = wp.transform(
312-
wp.vec3(joint_q[q_start + 0], joint_q[q_start + 1], joint_q[q_start + 2]),
313-
wp.quat(joint_q[q_start + 3], joint_q[q_start + 4], joint_q[q_start + 5], joint_q[q_start + 6]),
314-
)
315-
316-
v = wp.spatial_vector(
317-
wp.vec3(joint_qd[qd_start + 0], joint_qd[qd_start + 1], joint_qd[qd_start + 2]),
318-
wp.vec3(joint_qd[qd_start + 3], joint_qd[qd_start + 4], joint_qd[qd_start + 5]),
319-
)
433+
X_j, v_j = eval_joint_motion(
434+
type,
435+
q_start,
436+
qd_start,
437+
lin_axis_count,
438+
ang_axis_count,
439+
joint_q,
440+
joint_qd,
441+
joint_axis,
442+
)
320443

321-
X_j = t
322-
v_j = v
323-
324-
if type == JointType.D6:
325-
pos = wp.vec3(0.0)
326-
rot = wp.quat_identity()
327-
vel_v = wp.vec3(0.0)
328-
vel_w = wp.vec3(0.0)
329-
330-
# unroll for loop to ensure joint actions remain differentiable
331-
# (since differentiating through a for loop that updates a local variable is not supported)
332-
333-
if lin_axis_count > 0:
334-
axis = joint_axis[qd_start + 0]
335-
pos += axis * joint_q[q_start + 0]
336-
vel_v += axis * joint_qd[qd_start + 0]
337-
if lin_axis_count > 1:
338-
axis = joint_axis[qd_start + 1]
339-
pos += axis * joint_q[q_start + 1]
340-
vel_v += axis * joint_qd[qd_start + 1]
341-
if lin_axis_count > 2:
342-
axis = joint_axis[qd_start + 2]
343-
pos += axis * joint_q[q_start + 2]
344-
vel_v += axis * joint_qd[qd_start + 2]
345-
346-
iq = q_start + lin_axis_count
347-
iqd = qd_start + lin_axis_count
348-
if ang_axis_count == 1:
349-
axis = joint_axis[iqd]
350-
rot = wp.quat_from_axis_angle(axis, joint_q[iq])
351-
vel_w = joint_qd[iqd] * axis
352-
if ang_axis_count == 2:
353-
rot, vel_w = compute_2d_rotational_dofs(
354-
joint_axis[iqd + 0],
355-
joint_axis[iqd + 1],
356-
joint_q[iq + 0],
357-
joint_q[iq + 1],
358-
joint_qd[iqd + 0],
359-
joint_qd[iqd + 1],
360-
)
361-
if ang_axis_count == 3:
362-
rot, vel_w = compute_3d_rotational_dofs(
363-
joint_axis[iqd + 0],
364-
joint_axis[iqd + 1],
365-
joint_axis[iqd + 2],
366-
joint_q[iq + 0],
367-
joint_q[iq + 1],
368-
joint_q[iq + 2],
369-
joint_qd[iqd + 0],
370-
joint_qd[iqd + 1],
371-
joint_qd[iqd + 2],
372-
)
373-
374-
X_j = wp.transform(pos, rot)
375-
v_j = wp.spatial_vector(vel_v, vel_w)
376-
377-
# transform from world to parent joint anchor frame
378-
X_wpj = X_pj
444+
X_wp = wp.transform_identity()
379445
if parent >= 0:
380446
X_wp = body_q[parent]
381-
X_wpj = X_wp * X_wpj
382-
383-
# transform from world to joint anchor frame at child body
384-
X_wcj = X_wpj * X_j
385-
# transform from world to child body frame
386-
X_wc = X_wcj * wp.transform_inverse(X_cj)
387-
388-
# Velocity must be evaluated at the actual child-body origin. For translated
389-
# joints, sampling parent motion only at the fixed
390-
# parent anchor misses the transport term from the current joint displacement.
391-
x_child_origin = wp.transform_get_translation(X_wc)
392-
v_parent_origin = wp.vec3()
393-
w_parent = wp.vec3()
394-
if parent >= 0:
395-
v_wp = body_qd[parent]
396-
w_parent = wp.spatial_bottom(v_wp)
397-
v_parent_origin = com_twist_to_point_velocity(v_wp, X_wp, body_com[parent], x_child_origin)
398-
399-
# Transform joint motion into world space.
400-
linear_joint_world = wp.transform_vector(X_wpj, wp.spatial_top(v_j))
401-
angular_joint_world = wp.transform_vector(X_wpj, wp.spatial_bottom(v_j))
402-
if type == JointType.FREE or type == JointType.DISTANCE:
403-
# FREE / DISTANCE joint linear DOFs follow Newton's COM-velocity
404-
# convention, so convert the relative child COM twist to an
405-
# origin-referenced twist before the tree recurrence.
406-
v_joint_origin = com_twist_to_origin_twist(
407-
wp.spatial_vector(linear_joint_world, angular_joint_world),
408-
X_wc,
409-
body_com[child],
410-
)
411-
linear_joint_origin = wp.spatial_top(v_joint_origin)
412-
else:
413-
# The linear part of v_j is defined at the child joint anchor; if the
414-
# child body origin is offset from that anchor, transport the joint
415-
# angular motion to the body origin.
416-
child_origin_offset_world = x_child_origin - wp.transform_get_translation(X_wcj)
417-
linear_joint_origin = linear_joint_world + wp.cross(angular_joint_world, child_origin_offset_world)
418-
419-
v_wc_origin = wp.spatial_vector(v_parent_origin + linear_joint_origin, w_parent + angular_joint_world)
447+
X_wc, v_wc = eval_joint_child_state(
448+
type,
449+
parent,
450+
child,
451+
X_wp,
452+
X_pj,
453+
X_cj,
454+
X_j,
455+
v_j,
456+
body_qd,
457+
body_com,
458+
)
420459

421460
if (body_flags[child] & body_flag_filter) != 0:
422461
body_q[child] = X_wc
423-
body_qd[child] = origin_twist_to_com_twist(v_wc_origin, X_wc, body_com[child])
462+
body_qd[child] = v_wc
424463

425464

426465
@wp.kernel
@@ -540,6 +579,62 @@ def eval_fk(
540579
else:
541580
num_articulations = model.articulation_count
542581

582+
if num_articulations == 0:
583+
return
584+
585+
if model.device.is_cuda and model._fk_articulation_level_start is not None:
586+
requires_grad = (
587+
model.requires_grad
588+
or joint_q.requires_grad
589+
or joint_qd.requires_grad
590+
or model.joint_X_p.requires_grad
591+
or model.joint_X_c.requires_grad
592+
or model.joint_axis.requires_grad
593+
or model.body_com.requires_grad
594+
or state.body_q.requires_grad
595+
or state.body_qd.requires_grad
596+
)
597+
if not requires_grad:
598+
from .articulation_cuda import TILE_BLOCK_DIM, create_eval_articulation_fk_tile # noqa: PLC0415
599+
600+
kernel = create_eval_articulation_fk_tile(
601+
model._fk_level_capacity,
602+
body_flag_filter == BodyFlags.ALL,
603+
model._has_rod_joints,
604+
)
605+
inputs = [
606+
model._fk_articulation_level_start,
607+
model._fk_level_joint_start,
608+
model._fk_level_joints,
609+
model._fk_level_parent_pos,
610+
model.articulation_count,
611+
mask,
612+
indices,
613+
joint_q,
614+
joint_qd,
615+
model.joint_q_start,
616+
model.joint_qd_start,
617+
model.joint_type,
618+
model.joint_parent,
619+
model.joint_child,
620+
model.joint_X_p,
621+
model.joint_X_c,
622+
model.joint_axis,
623+
model.joint_dof_dim,
624+
model.body_com,
625+
model.body_flags,
626+
body_flag_filter,
627+
]
628+
wp.launch_tiled(
629+
kernel=kernel,
630+
dim=[num_articulations],
631+
block_dim=TILE_BLOCK_DIM,
632+
inputs=inputs,
633+
outputs=[state.body_q, state.body_qd],
634+
device=model.device,
635+
)
636+
return
637+
543638
wp.launch(
544639
kernel=eval_articulation_fk,
545640
dim=num_articulations,

0 commit comments

Comments
 (0)