Skip to content

Commit acb3c85

Browse files
cdtwiggmeta-codesync[bot]
authored andcommitted
triton_fk: Triton backend for joint_parameters_to_local_skeleton_state (#1536)
Summary: Pull Request resolved: #1536 Adds a Triton local-FK to `triton_fk` and a `backend` param to `Skeleton.joint_parameters_to_local_skeleton_state` and `GpuCharacter.{joint,model}_parameters_to_local_skeleton_state` (default "torch"), matching the existing global FK (`joint_parameters_to_skeleton_state`). The local skeleton state is exactly the per-joint state the global FK computes BEFORE the prefix-product up the kinematic tree, so the new kernels reuse the existing device functions: the forward stores `_load_local_state`'s output (no parent composition), and the backward is `_store_joint_param_gradients` fed grad_output directly (no tree -- grad w.r.t. the local state IS grad_output per joint). On CUDA float32 `backend="auto"` opts into Triton; it falls back to torch under torch.jit.script/trace. Reviewed By: cstollmeta Differential Revision: D108091266 fbshipit-source-id: d95eb4415769d887f42683e4097351c5e04c1151
1 parent 8fe2f1e commit acb3c85

3 files changed

Lines changed: 339 additions & 14 deletions

File tree

pymomentum/backend/triton_fk.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,83 @@ def _backward_kernel(
715715
)
716716
joint -= 1
717717

718+
@triton.jit
719+
def _local_forward_kernel(
720+
joint_params,
721+
offsets,
722+
prerotations,
723+
output,
724+
batch_size,
725+
n_joints,
726+
BLOCK_BATCH: tl.constexpr,
727+
):
728+
# Local skeleton state: the per-joint state from _load_local_state, WITHOUT
729+
# the parent composition that _forward_one_joint applies (i.e. the global FK
730+
# before the prefix product up the kinematic tree).
731+
batch = tl.program_id(0) * BLOCK_BATCH + tl.arange(0, BLOCK_BATCH)
732+
mask = batch < batch_size
733+
joint = 0
734+
while joint < n_joints:
735+
tx, ty, tz, qx, qy, qz, qw, s = _load_local_state(
736+
joint_params, offsets, prerotations, batch, joint, n_joints, mask
737+
)
738+
out_base = output + batch * n_joints * 8 + joint * 8
739+
tl.store(out_base + 0, tx, mask=mask)
740+
tl.store(out_base + 1, ty, mask=mask)
741+
tl.store(out_base + 2, tz, mask=mask)
742+
tl.store(out_base + 3, qx, mask=mask)
743+
tl.store(out_base + 4, qy, mask=mask)
744+
tl.store(out_base + 5, qz, mask=mask)
745+
tl.store(out_base + 6, qw, mask=mask)
746+
tl.store(out_base + 7, s, mask=mask)
747+
joint += 1
748+
749+
@triton.jit
750+
def _local_backward_kernel(
751+
joint_params,
752+
offsets,
753+
prerotations,
754+
grad_output,
755+
grad_joint_params,
756+
batch_size,
757+
n_joints,
758+
BLOCK_BATCH: tl.constexpr,
759+
):
760+
# Backward of the local FK: there is no tree, so grad w.r.t. the local
761+
# skeleton state IS grad_output per joint. _store_joint_param_gradients
762+
# propagates it to joint_params (translation identity, rotation through the
763+
# prerotation*euler quaternion multiply + euler, scale through exp2).
764+
batch = tl.program_id(0) * BLOCK_BATCH + tl.arange(0, BLOCK_BATCH)
765+
mask = batch < batch_size
766+
joint = 0
767+
while joint < n_joints:
768+
_, _, _, _, _, _, _, ls = _load_local_state(
769+
joint_params, offsets, prerotations, batch, joint, n_joints, mask
770+
)
771+
gtx, gty, gtz, gqx, gqy, gqz, gqw, gs = _load_state(
772+
grad_output, batch, joint, n_joints, mask
773+
)
774+
_store_joint_param_gradients(
775+
joint_params,
776+
prerotations,
777+
grad_joint_params,
778+
batch,
779+
joint,
780+
n_joints,
781+
mask,
782+
True,
783+
gtx,
784+
gty,
785+
gtz,
786+
gqx,
787+
gqy,
788+
gqz,
789+
gqw,
790+
gs,
791+
ls,
792+
)
793+
joint += 1
794+
718795

719796
def _require_triton() -> None:
720797
if triton is None or tl is None:
@@ -907,3 +984,134 @@ def joint_parameters_to_skeleton_state(
907984
joint_parents,
908985
active_joints,
909986
)
987+
988+
989+
def _validate_local_inputs(
990+
joint_parameters: torch.Tensor,
991+
joint_translation_offsets: torch.Tensor,
992+
joint_prerotations: torch.Tensor,
993+
) -> None:
994+
# Same as _validate_inputs but the local FK needs no kinematic tree (parents).
995+
if not joint_parameters.is_cuda:
996+
raise RuntimeError("Triton local FK requested, but joint_parameters is on CPU.")
997+
if joint_parameters.dtype != torch.float32:
998+
raise RuntimeError(
999+
"Triton local FK currently only supports float32 joint parameters."
1000+
)
1001+
if joint_parameters.ndim < 2 or joint_parameters.shape[-1] != 7:
1002+
raise RuntimeError("joint_parameters must have shape [..., num_joints, 7].")
1003+
if joint_translation_offsets.device != joint_parameters.device:
1004+
raise RuntimeError("joint_translation_offsets must be on the same device.")
1005+
if joint_prerotations.device != joint_parameters.device:
1006+
raise RuntimeError("joint_prerotations must be on the same device.")
1007+
if joint_translation_offsets.dtype != torch.float32:
1008+
raise RuntimeError("joint_translation_offsets must be float32.")
1009+
if joint_prerotations.dtype != torch.float32:
1010+
raise RuntimeError("joint_prerotations must be float32.")
1011+
num_joints = joint_parameters.shape[-2]
1012+
if joint_translation_offsets.shape != (num_joints, 3):
1013+
raise RuntimeError("joint_translation_offsets must have shape [num_joints, 3].")
1014+
if joint_prerotations.shape != (num_joints, 4):
1015+
raise RuntimeError("joint_prerotations must have shape [num_joints, 4].")
1016+
1017+
1018+
def _launch_local_forward(
1019+
joint_parameters: torch.Tensor,
1020+
joint_translation_offsets: torch.Tensor,
1021+
joint_prerotations: torch.Tensor,
1022+
) -> torch.Tensor:
1023+
_validate_local_inputs(
1024+
joint_parameters, joint_translation_offsets, joint_prerotations
1025+
)
1026+
_require_triton()
1027+
num_joints = joint_parameters.shape[-2]
1028+
batch_size = joint_parameters.numel() // (num_joints * 7)
1029+
output = torch.empty(
1030+
(*joint_parameters.shape[:-1], 8),
1031+
device=joint_parameters.device,
1032+
dtype=joint_parameters.dtype,
1033+
)
1034+
grid = (triton.cdiv(batch_size, _BLOCK_BATCH),)
1035+
_local_forward_kernel[grid](
1036+
joint_parameters,
1037+
joint_translation_offsets,
1038+
joint_prerotations,
1039+
output,
1040+
batch_size,
1041+
num_joints,
1042+
BLOCK_BATCH=_BLOCK_BATCH,
1043+
)
1044+
return output
1045+
1046+
1047+
def _launch_local_backward(
1048+
joint_parameters: torch.Tensor,
1049+
joint_translation_offsets: torch.Tensor,
1050+
joint_prerotations: torch.Tensor,
1051+
grad_output: torch.Tensor,
1052+
) -> torch.Tensor:
1053+
_require_triton()
1054+
grad_output = grad_output.contiguous()
1055+
grad_joint_parameters = torch.empty_like(joint_parameters)
1056+
num_joints = joint_parameters.shape[-2]
1057+
batch_size = joint_parameters.numel() // (num_joints * 7)
1058+
grid = (triton.cdiv(batch_size, _BLOCK_BATCH),)
1059+
_local_backward_kernel[grid](
1060+
joint_parameters,
1061+
joint_translation_offsets,
1062+
joint_prerotations,
1063+
grad_output,
1064+
grad_joint_parameters,
1065+
batch_size,
1066+
num_joints,
1067+
BLOCK_BATCH=_BLOCK_BATCH,
1068+
)
1069+
return grad_joint_parameters
1070+
1071+
1072+
class _JointParametersToLocalSkeletonState(torch.autograd.Function):
1073+
@staticmethod
1074+
def forward(
1075+
ctx: torch.autograd.function.FunctionCtx,
1076+
joint_parameters: torch.Tensor,
1077+
joint_translation_offsets: torch.Tensor,
1078+
joint_prerotations: torch.Tensor,
1079+
) -> torch.Tensor:
1080+
joint_parameters = joint_parameters.contiguous()
1081+
joint_translation_offsets = joint_translation_offsets.contiguous()
1082+
joint_prerotations = joint_prerotations.contiguous()
1083+
output = _launch_local_forward(
1084+
joint_parameters, joint_translation_offsets, joint_prerotations
1085+
)
1086+
ctx.save_for_backward(
1087+
joint_parameters, joint_translation_offsets, joint_prerotations
1088+
)
1089+
return output
1090+
1091+
@staticmethod
1092+
def backward(
1093+
ctx: torch.autograd.function.FunctionCtx,
1094+
grad_output: torch.Tensor,
1095+
) -> tuple[torch.Tensor | None, None, None]:
1096+
joint_parameters, joint_translation_offsets, joint_prerotations = (
1097+
ctx.saved_tensors
1098+
)
1099+
grad_joint_parameters = _launch_local_backward(
1100+
joint_parameters,
1101+
joint_translation_offsets,
1102+
joint_prerotations,
1103+
grad_output,
1104+
)
1105+
return grad_joint_parameters, None, None
1106+
1107+
1108+
def joint_parameters_to_local_skeleton_state(
1109+
joint_parameters: torch.Tensor,
1110+
joint_translation_offsets: torch.Tensor,
1111+
joint_prerotations: torch.Tensor,
1112+
) -> torch.Tensor:
1113+
return _JointParametersToLocalSkeletonState.apply(
1114+
joint_parameters,
1115+
joint_translation_offsets,
1116+
joint_prerotations,
1117+
)

pymomentum/test/test_triton_fk_backend.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,80 @@ def test_fk_with_model_parameters_matches_torch(self) -> None:
154154
triton_state.backward(grad_output)
155155
self._assert_grad_close(triton_params, torch_params)
156156

157+
def test_local_fk_with_model_parameters_matches_torch(self) -> None:
158+
device = _cuda_device()
159+
character_cpu = pym_test_utils.create_test_character()
160+
character = pym_character.Character(
161+
character_cpu,
162+
has_parameter_transform=True,
163+
has_skeleton=True,
164+
has_rest_mesh=False,
165+
has_skinning=False,
166+
).to(device)
167+
168+
torch.manual_seed(0)
169+
model_params = torch.rand(
170+
5,
171+
character_cpu.parameter_transform.size,
172+
device=device,
173+
dtype=torch.float32,
174+
)
175+
176+
torch_params = model_params.detach().clone().requires_grad_()
177+
triton_params = model_params.detach().clone().requires_grad_()
178+
179+
torch_state = character.model_parameters_to_local_skeleton_state(
180+
torch_params,
181+
backend="torch",
182+
)
183+
triton_state = character.model_parameters_to_local_skeleton_state(
184+
triton_params,
185+
backend="triton",
186+
)
187+
188+
torch.testing.assert_close(torch_state, triton_state, atol=1e-5, rtol=1e-5)
189+
190+
grad_output = torch.randn_like(torch_state)
191+
torch_state.backward(grad_output)
192+
triton_state.backward(grad_output)
193+
self._assert_grad_close(triton_params, torch_params)
194+
195+
def test_local_fk_matches_torch(self) -> None:
196+
# Direct joint-parameter local FK (no parameter transform), exercising the
197+
# _local_forward_kernel / _local_backward_kernel without depending on the
198+
# model-parameter transform. Covers both a single and a multi-dim batch.
199+
device = _cuda_device()
200+
character_cpu = pym_test_utils.create_test_character()
201+
character = pym_character.Character(
202+
character_cpu,
203+
has_parameter_transform=False,
204+
has_skeleton=True,
205+
has_rest_mesh=False,
206+
has_skinning=False,
207+
).to(device)
208+
209+
torch.manual_seed(0)
210+
joint_params = torch.rand(
211+
5,
212+
character_cpu.skeleton.size,
213+
7,
214+
device=device,
215+
dtype=torch.float32,
216+
)
217+
self._assert_local_joint_parameter_backend_close(character, joint_params)
218+
219+
batched_joint_params = torch.rand(
220+
2,
221+
3,
222+
character_cpu.skeleton.size,
223+
7,
224+
device=device,
225+
dtype=torch.float32,
226+
)
227+
self._assert_local_joint_parameter_backend_close(
228+
character, batched_joint_params
229+
)
230+
157231
def test_inverse_fk_matches_torch(self) -> None:
158232
device = _cuda_device()
159233
character_cpu = pym_test_utils.create_test_character()
@@ -224,6 +298,30 @@ def _assert_joint_parameter_backend_close(
224298
triton_state.backward(grad_output)
225299
self._assert_grad_close(triton_params, torch_params)
226300

301+
def _assert_local_joint_parameter_backend_close(
302+
self,
303+
character: pym_character.Character,
304+
joint_params: torch.Tensor,
305+
) -> None:
306+
torch_params = joint_params.detach().clone().requires_grad_()
307+
triton_params = joint_params.detach().clone().requires_grad_()
308+
309+
torch_state = character.joint_parameters_to_local_skeleton_state(
310+
torch_params,
311+
backend="torch",
312+
)
313+
triton_state = character.joint_parameters_to_local_skeleton_state(
314+
triton_params,
315+
backend="triton",
316+
)
317+
318+
torch.testing.assert_close(torch_state, triton_state, atol=1e-5, rtol=1e-5)
319+
320+
grad_output = torch.randn_like(torch_state)
321+
torch_state.backward(grad_output)
322+
triton_state.backward(grad_output)
323+
self._assert_grad_close(triton_params, torch_params)
324+
227325
def _assert_grad_close(
228326
self,
229327
triton_tensor: torch.Tensor,

0 commit comments

Comments
 (0)