Skip to content

Commit 59bb31f

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 18 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 59bb31f

6 files changed

Lines changed: 70 additions & 44 deletions

File tree

genesis/__init__.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,43 @@
5252
EPS: float | None = None
5353

5454

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

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

genesis/engine/solvers/kinematic_solver.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -801,8 +801,7 @@ 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()
804+
806805
else:
807806
qpos, qs_idx, envs_idx = self._sanitize_io_variables(
808807
qpos, qs_idx, self.n_qs, "qs_idx", envs_idx, skip_allocation=True
@@ -869,8 +868,7 @@ def set_dofs_velocity(self, velocity, dofs_idx=None, envs_idx=None, *, skip_forw
869868
assign_indexed_tensor(vel, mask, velocity)
870869
if mask and isinstance(mask[0], torch.Tensor):
871870
envs_idx = mask[0].reshape((-1,))
872-
if gs.backend == gs.metal:
873-
torch.mps.synchronize()
871+
874872
if not skip_forward and not isinstance(envs_idx, torch.Tensor):
875873
envs_idx = self._scene._sanitize_envs_idx(envs_idx)
876874
else:

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,8 +581,7 @@ 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()
584+
586585
return
587586

588587
envs_idx = self._solver._scene._sanitize_envs_idx(envs_idx)
@@ -636,8 +635,7 @@ def clear(self, envs_idx=None):
636635
pos[:, envs_idx] = 0.0
637636
normal[:, envs_idx] = 0.0
638637
force[:, envs_idx] = 0.0
639-
if gs.backend == gs.metal:
640-
torch.mps.synchronize()
638+
641639
return
642640

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

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,7 @@ 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()
155+
157156
return
158157

159158
envs_idx = self._solver._scene._sanitize_envs_idx(envs_idx)
@@ -186,8 +185,7 @@ def clear(self, envs_idx=None):
186185
assign_indexed_tensor(n_constraints_equality, env_mask, 0)
187186
assign_indexed_tensor(n_constraints_frictionloss, env_mask, 0)
188187
assign_indexed_tensor(qd_n_equalities, env_mask, n_eq)
189-
if gs.backend == gs.metal:
190-
torch.mps.synchronize()
188+
191189
return
192190

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

genesis/engine/solvers/rigid/rigid_solver.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,8 +1568,7 @@ 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()
1571+
15731572
else:
15741573
envs_idx = self._scene._sanitize_envs_idx(envs_idx)
15751574
kernel_set_zero(envs_idx, self._errno)
@@ -1696,8 +1695,7 @@ def set_base_links_pos(self, pos, links_idx=None, envs_idx=None, *, relative=Fal
16961695
target = data[:, link.q_start : link.q_start + 3]
16971696
pos = broadcast_tensor(pos, gs.tc_float, target.shape)
16981697
torch.where(envs_idx[:, None], pos, target, out=target)
1699-
if gs.backend == gs.metal:
1700-
torch.mps.synchronize()
1698+
17011699
else:
17021700
pos, links_idx, envs_idx = self._sanitize_io_variables(
17031701
pos, links_idx, self.n_links, "links_idx", envs_idx, (3,), skip_allocation=True
@@ -1795,8 +1793,7 @@ def set_base_links_quat(self, quat, links_idx=None, envs_idx=None, *, relative=F
17951793
target = data[:, link.q_start + 3 : link.q_start + 7]
17961794
quat = broadcast_tensor(quat, gs.tc_float, target.shape)
17971795
torch.where(envs_idx[:, None], quat, target, out=target)
1798-
if gs.backend == gs.metal:
1799-
torch.mps.synchronize()
1796+
18001797
else:
18011798
quat, links_idx, envs_idx = self._sanitize_io_variables(
18021799
quat, links_idx, self.n_links, "links_idx", envs_idx, (4,), skip_allocation=True
@@ -1915,8 +1912,7 @@ def set_links_inertia(self, ratio, links_idx=None, envs_idx=None):
19151912
assign_indexed_tensor(mass_data, mask, mass_data[mask] * ratio_t)
19161913
assign_indexed_tensor(inertial_i_data, mask, inertial_i_data[mask] * ratio_t[..., None, None])
19171914
assign_indexed_tensor(invweight_data, mask, invweight_data[mask] / ratio_t[..., None])
1918-
if gs.backend == gs.metal:
1919-
torch.mps.synchronize()
1915+
19201916
return
19211917

19221918
ratio, links_idx, envs_idx = self._sanitize_io_variables(
@@ -1971,8 +1967,7 @@ def set_qpos(self, qpos, qs_idx=None, envs_idx=None, *, skip_forward=False):
19711967
errno[envs_idx] = 0
19721968
if mask and isinstance(mask[0], torch.Tensor):
19731969
envs_idx = mask[0].reshape((-1,))
1974-
if gs.backend == gs.metal:
1975-
torch.mps.synchronize()
1970+
19761971
else:
19771972
qpos, qs_idx, envs_idx = self._sanitize_io_variables(
19781973
qpos, qs_idx, self.n_qs, "qs_idx", envs_idx, skip_allocation=True
@@ -2116,8 +2111,7 @@ def _set_dofs_info(self, tensor_list, dofs_idx, name, envs_idx=None):
21162111
num_values = len(tensor_list)
21172112
for j, mask_j in enumerate(((*mask, ..., j) for j in range(num_values)) if num_values > 1 else (mask,)):
21182113
assign_indexed_tensor(data, mask_j, tensor_list[j])
2119-
if gs.backend == gs.metal:
2120-
torch.mps.synchronize()
2114+
21212115
return
21222116

21232117
tensor_list = list(tensor_list)
@@ -2219,8 +2213,7 @@ def set_dofs_position(self, position, dofs_idx=None, envs_idx=None):
22192213
if gs.use_zerocopy:
22202214
errno = qd_to_torch(self._errno, copy=False)
22212215
errno[envs_idx] = 0
2222-
if gs.backend == gs.metal:
2223-
torch.mps.synchronize()
2216+
22242217
else:
22252218
kernel_set_zero(envs_idx, self._errno)
22262219

@@ -2248,8 +2241,7 @@ def control_dofs_force(self, force, dofs_idx=None, envs_idx=None):
22482241
ctrl_mode[mask] = gs.CTRL_MODE.FORCE
22492242
ctrl_force = qd_to_torch(self.dofs_state.ctrl_force, transpose=True, copy=False)
22502243
assign_indexed_tensor(ctrl_force, mask, force)
2251-
if gs.backend == gs.metal:
2252-
torch.mps.synchronize()
2244+
22532245
return
22542246

22552247
force, dofs_idx, envs_idx = self._sanitize_io_variables(
@@ -2269,8 +2261,7 @@ def control_dofs_velocity(self, velocity, dofs_idx=None, envs_idx=None):
22692261
ctrl_pos[mask] = 0.0
22702262
ctrl_vel = qd_to_torch(self.dofs_state.ctrl_vel, transpose=True, copy=False)
22712263
assign_indexed_tensor(ctrl_vel, mask, velocity)
2272-
if gs.backend == gs.metal:
2273-
torch.mps.synchronize()
2264+
22742265
return
22752266

22762267
velocity, dofs_idx, envs_idx = self._sanitize_io_variables(
@@ -2290,8 +2281,7 @@ def control_dofs_position(self, position, dofs_idx=None, envs_idx=None):
22902281
assign_indexed_tensor(ctrl_pos, mask, position)
22912282
ctrl_vel = qd_to_torch(self.dofs_state.ctrl_vel, transpose=True, copy=False)
22922283
ctrl_vel[mask] = 0.0
2293-
if gs.backend == gs.metal:
2294-
torch.mps.synchronize()
2284+
22952285
return
22962286

22972287
position, dofs_idx, envs_idx = self._sanitize_io_variables(
@@ -2311,8 +2301,7 @@ def control_dofs_position_velocity(self, position, velocity, dofs_idx=None, envs
23112301
assign_indexed_tensor(ctrl_pos, mask, position)
23122302
ctrl_vel = qd_to_torch(self.dofs_state.ctrl_vel, transpose=True, copy=False)
23132303
assign_indexed_tensor(ctrl_vel, mask, velocity)
2314-
if gs.backend == gs.metal:
2315-
torch.mps.synchronize()
2304+
23162305
return
23172306

23182307
position, dofs_idx, _ = self._sanitize_io_variables(
@@ -2648,8 +2637,7 @@ def clear_external_force(self):
26482637
for tensor in (self.links_state.cfrc_applied_ang, self.links_state.cfrc_applied_vel):
26492638
out = qd_to_torch(tensor, copy=False)
26502639
out.zero_()
2651-
if gs.backend == gs.metal:
2652-
torch.mps.synchronize()
2640+
26532641
return
26542642

26552643
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)