Skip to content

Commit 3d0330c

Browse files
[FEATURE] Share Metal command queue between Quadrants and PyTorch MPS
At gs.init() time on Apple Metal, extract PyTorch MPS's MTLCommandQueue and pass it to Quadrants via external_metal_command_queue. This lets both frameworks dispatch GPU work on the same queue, so Metal's sequential command buffer ordering eliminates the need for explicit CPU-side syncs (qd.sync / torch.mps.synchronize) at every interop point. Raises an exception if the queue cannot be extracted, since correct synchronisation on Metal depends on the shared queue. Removes all torch.mps.synchronize() guards from solvers and misc.py. Depends on Genesis-Embodied-AI/quadrants#618. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2f0feff commit 3d0330c

6 files changed

Lines changed: 51 additions & 44 deletions

File tree

genesis/__init__.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ctypes
12
import io
23
import os
34
import sys
@@ -52,6 +53,41 @@
5253
EPS: float | None = None
5354

5455

56+
def _get_mps_command_queue() -> int:
57+
"""Extract PyTorch MPS's MTLCommandQueue* as a Python int, or 0 on failure."""
58+
try:
59+
torch_lib = os.path.join(os.path.dirname(torch.__file__), "lib", "libtorch_cpu.dylib")
60+
handle = ctypes.CDLL(torch_lib)._handle
61+
62+
libdl = ctypes.CDLL(None)
63+
dlsym = libdl.dlsym
64+
dlsym.restype = ctypes.c_void_p
65+
dlsym.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
66+
67+
stream_fn = dlsym(handle, b"_ZN2at3mps19getDefaultMPSStreamEv")
68+
if not stream_fn:
69+
return 0
70+
stream_ptr = ctypes.CFUNCTYPE(ctypes.c_void_p)(stream_fn)()
71+
72+
cb_fn = dlsym(handle, b"_ZN2at3mps9MPSStream13commandBufferEv")
73+
if not cb_fn:
74+
return 0
75+
cb_ptr = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(cb_fn)(stream_ptr)
76+
77+
objc = ctypes.CDLL("/usr/lib/libobjc.A.dylib")
78+
sel_reg = objc.sel_registerName
79+
sel_reg.restype = ctypes.c_void_p
80+
sel_reg.argtypes = [ctypes.c_char_p]
81+
msg_send = objc.objc_msgSend
82+
msg_send.restype = ctypes.c_void_p
83+
msg_send.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
84+
85+
queue_ptr = msg_send(cb_ptr, sel_reg(b"commandQueue"))
86+
return queue_ptr or 0
87+
except (OSError, AttributeError):
88+
return 0
89+
90+
5591
########################## init ##########################
5692
def init(
5793
*,
@@ -255,6 +291,21 @@ def init(
255291
random_seed=seed,
256292
)
257293

294+
# On Metal, share PyTorch MPS's command queue with Quadrants so that GPU-side ordering is automatic and the
295+
# per-interop-point sync overhead (qd.sync / torch.mps.synchronize) is eliminated.
296+
if backend == _gs_backend.metal and device.type == "mps":
297+
mps_queue = _get_mps_command_queue()
298+
if not mps_queue:
299+
raise_exception(
300+
"Failed to extract PyTorch MPS's Metal command queue. "
301+
"This is required on Apple Metal for correct GPU synchronisation between Genesis and PyTorch. "
302+
"Please ensure you are using a supported PyTorch version (>= 2.0)."
303+
)
304+
qd_init_kwargs.update(
305+
external_metal_command_queue=mps_queue,
306+
external_metal_command_queue_is_torch_queue=True,
307+
)
308+
258309
# init quadrants
259310
qd_debug = debug and (os.environ.get("QD_DEBUG") != "0")
260311
with redirect_stdout(_qd_outputs):

genesis/engine/solvers/kinematic_solver.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -801,8 +801,6 @@ def set_qpos(self, qpos, qs_idx=None, envs_idx=None, *, skip_forward=False):
801801
assign_indexed_tensor(data, mask, qpos)
802802
if mask and isinstance(mask[0], torch.Tensor):
803803
envs_idx = mask[0].reshape((-1,))
804-
if gs.backend == gs.metal:
805-
torch.mps.synchronize()
806804
else:
807805
qpos, qs_idx, envs_idx = self._sanitize_io_variables(
808806
qpos, qs_idx, self.n_qs, "qs_idx", envs_idx, skip_allocation=True
@@ -869,8 +867,6 @@ def set_dofs_velocity(self, velocity, dofs_idx=None, envs_idx=None, *, skip_forw
869867
assign_indexed_tensor(vel, mask, velocity)
870868
if mask and isinstance(mask[0], torch.Tensor):
871869
envs_idx = mask[0].reshape((-1,))
872-
if gs.backend == gs.metal:
873-
torch.mps.synchronize()
874870
if not skip_forward and not isinstance(envs_idx, torch.Tensor):
875871
envs_idx = self._scene._sanitize_envs_idx(envs_idx)
876872
else:

genesis/engine/solvers/rigid/collider/collider.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,6 @@ def reset(self, envs_idx=None, *, cache_only: bool = True) -> None:
581581
normal.zero_()
582582
else:
583583
normal[:, envs_idx] = 0.0
584-
if gs.backend == gs.metal:
585-
torch.mps.synchronize()
586584
return
587585

588586
envs_idx = self._solver._scene._sanitize_envs_idx(envs_idx)
@@ -636,8 +634,6 @@ def clear(self, envs_idx=None):
636634
pos[:, envs_idx] = 0.0
637635
normal[:, envs_idx] = 0.0
638636
force[:, envs_idx] = 0.0
639-
if gs.backend == gs.metal:
640-
torch.mps.synchronize()
641637
return
642638

643639
if not isinstance(envs_idx, torch.Tensor):

genesis/engine/solvers/rigid/constraint/solver.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,6 @@ def reset(self, envs_idx=None):
152152
else:
153153
is_warmstart[envs_idx] = False
154154
qacc_ws[:, envs_idx] = 0.0
155-
if gs.backend == gs.metal:
156-
torch.mps.synchronize()
157155
return
158156

159157
envs_idx = self._solver._scene._sanitize_envs_idx(envs_idx)
@@ -186,8 +184,6 @@ def clear(self, envs_idx=None):
186184
assign_indexed_tensor(n_constraints_equality, env_mask, 0)
187185
assign_indexed_tensor(n_constraints_frictionloss, env_mask, 0)
188186
assign_indexed_tensor(qd_n_equalities, env_mask, n_eq)
189-
if gs.backend == gs.metal:
190-
torch.mps.synchronize()
191187
return
192188

193189
if not isinstance(envs_idx, torch.Tensor):

genesis/engine/solvers/rigid/rigid_solver.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,8 +1568,6 @@ def set_state(self, f, state, envs_idx=None, *, partial: bool = False) -> None:
15681568
mass_dst[envs_idx] = state.mass_shift[envs_idx]
15691569
if self.n_geoms:
15701570
fric_dst[envs_idx] = state.friction_ratio[envs_idx]
1571-
if gs.backend == gs.metal:
1572-
torch.mps.synchronize()
15731571
else:
15741572
envs_idx = self._scene._sanitize_envs_idx(envs_idx)
15751573
kernel_set_zero(envs_idx, self._errno)
@@ -1696,8 +1694,6 @@ def set_base_links_pos(self, pos, links_idx=None, envs_idx=None, *, relative=Fal
16961694
target = data[:, link.q_start : link.q_start + 3]
16971695
pos = broadcast_tensor(pos, gs.tc_float, target.shape)
16981696
torch.where(envs_idx[:, None], pos, target, out=target)
1699-
if gs.backend == gs.metal:
1700-
torch.mps.synchronize()
17011697
else:
17021698
pos, links_idx, envs_idx = self._sanitize_io_variables(
17031699
pos, links_idx, self.n_links, "links_idx", envs_idx, (3,), skip_allocation=True
@@ -1795,8 +1791,6 @@ def set_base_links_quat(self, quat, links_idx=None, envs_idx=None, *, relative=F
17951791
target = data[:, link.q_start + 3 : link.q_start + 7]
17961792
quat = broadcast_tensor(quat, gs.tc_float, target.shape)
17971793
torch.where(envs_idx[:, None], quat, target, out=target)
1798-
if gs.backend == gs.metal:
1799-
torch.mps.synchronize()
18001794
else:
18011795
quat, links_idx, envs_idx = self._sanitize_io_variables(
18021796
quat, links_idx, self.n_links, "links_idx", envs_idx, (4,), skip_allocation=True
@@ -1915,8 +1909,6 @@ def set_links_inertia(self, ratio, links_idx=None, envs_idx=None):
19151909
assign_indexed_tensor(mass_data, mask, mass_data[mask] * ratio_t)
19161910
assign_indexed_tensor(inertial_i_data, mask, inertial_i_data[mask] * ratio_t[..., None, None])
19171911
assign_indexed_tensor(invweight_data, mask, invweight_data[mask] / ratio_t[..., None])
1918-
if gs.backend == gs.metal:
1919-
torch.mps.synchronize()
19201912
return
19211913

19221914
ratio, links_idx, envs_idx = self._sanitize_io_variables(
@@ -1971,8 +1963,6 @@ def set_qpos(self, qpos, qs_idx=None, envs_idx=None, *, skip_forward=False):
19711963
errno[envs_idx] = 0
19721964
if mask and isinstance(mask[0], torch.Tensor):
19731965
envs_idx = mask[0].reshape((-1,))
1974-
if gs.backend == gs.metal:
1975-
torch.mps.synchronize()
19761966
else:
19771967
qpos, qs_idx, envs_idx = self._sanitize_io_variables(
19781968
qpos, qs_idx, self.n_qs, "qs_idx", envs_idx, skip_allocation=True
@@ -2116,8 +2106,6 @@ def _set_dofs_info(self, tensor_list, dofs_idx, name, envs_idx=None):
21162106
num_values = len(tensor_list)
21172107
for j, mask_j in enumerate(((*mask, ..., j) for j in range(num_values)) if num_values > 1 else (mask,)):
21182108
assign_indexed_tensor(data, mask_j, tensor_list[j])
2119-
if gs.backend == gs.metal:
2120-
torch.mps.synchronize()
21212109
return
21222110

21232111
tensor_list = list(tensor_list)
@@ -2219,8 +2207,6 @@ def set_dofs_position(self, position, dofs_idx=None, envs_idx=None):
22192207
if gs.use_zerocopy:
22202208
errno = qd_to_torch(self._errno, copy=False)
22212209
errno[envs_idx] = 0
2222-
if gs.backend == gs.metal:
2223-
torch.mps.synchronize()
22242210
else:
22252211
kernel_set_zero(envs_idx, self._errno)
22262212

@@ -2248,8 +2234,6 @@ def control_dofs_force(self, force, dofs_idx=None, envs_idx=None):
22482234
ctrl_mode[mask] = gs.CTRL_MODE.FORCE
22492235
ctrl_force = qd_to_torch(self.dofs_state.ctrl_force, transpose=True, copy=False)
22502236
assign_indexed_tensor(ctrl_force, mask, force)
2251-
if gs.backend == gs.metal:
2252-
torch.mps.synchronize()
22532237
return
22542238

22552239
force, dofs_idx, envs_idx = self._sanitize_io_variables(
@@ -2269,8 +2253,6 @@ def control_dofs_velocity(self, velocity, dofs_idx=None, envs_idx=None):
22692253
ctrl_pos[mask] = 0.0
22702254
ctrl_vel = qd_to_torch(self.dofs_state.ctrl_vel, transpose=True, copy=False)
22712255
assign_indexed_tensor(ctrl_vel, mask, velocity)
2272-
if gs.backend == gs.metal:
2273-
torch.mps.synchronize()
22742256
return
22752257

22762258
velocity, dofs_idx, envs_idx = self._sanitize_io_variables(
@@ -2290,8 +2272,6 @@ def control_dofs_position(self, position, dofs_idx=None, envs_idx=None):
22902272
assign_indexed_tensor(ctrl_pos, mask, position)
22912273
ctrl_vel = qd_to_torch(self.dofs_state.ctrl_vel, transpose=True, copy=False)
22922274
ctrl_vel[mask] = 0.0
2293-
if gs.backend == gs.metal:
2294-
torch.mps.synchronize()
22952275
return
22962276

22972277
position, dofs_idx, envs_idx = self._sanitize_io_variables(
@@ -2311,8 +2291,6 @@ def control_dofs_position_velocity(self, position, velocity, dofs_idx=None, envs
23112291
assign_indexed_tensor(ctrl_pos, mask, position)
23122292
ctrl_vel = qd_to_torch(self.dofs_state.ctrl_vel, transpose=True, copy=False)
23132293
assign_indexed_tensor(ctrl_vel, mask, velocity)
2314-
if gs.backend == gs.metal:
2315-
torch.mps.synchronize()
23162294
return
23172295

23182296
position, dofs_idx, _ = self._sanitize_io_variables(
@@ -2648,8 +2626,6 @@ def clear_external_force(self):
26482626
for tensor in (self.links_state.cfrc_applied_ang, self.links_state.cfrc_applied_vel):
26492627
out = qd_to_torch(tensor, copy=False)
26502628
out.zero_()
2651-
if gs.backend == gs.metal:
2652-
torch.mps.synchronize()
26532629
return
26542630

26552631
kernel_clear_external_force(self.links_state, self._rigid_global_info, self._static_rigid_sim_config)

genesis/utils/misc.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -534,14 +534,6 @@ def qd_to_torch(
534534
else:
535535
try:
536536
tensor = value._T_tc if transpose else value._tc
537-
# FIXME: DLPack may return old values on Apple Metal if sync is not systematically called manually.
538-
# See comment in `qd_to_python` for details on the MPS command queue race condition.
539-
if gs.backend == gs.metal:
540-
qd.sync()
541-
if copy:
542-
tensor = tensor.clone()
543-
if gs.backend == gs.metal:
544-
torch.mps.synchronize()
545537
except AttributeError:
546538
try:
547539
tc = value.to_torch(copy=False)

0 commit comments

Comments
 (0)