Skip to content

Commit bc79efc

Browse files
[PERF] Cache Metal interop sync decision per runtime instance
Avoid repeated pybind attribute lookups on the hot path by caching the result of the arch + external_metal_command_queue check. The cache is keyed on the runtime object identity so it auto-invalidates on qd.init() / qd.reset(). Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent aa592c1 commit bc79efc

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

python/quadrants/lang/field.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,37 @@ def _patch_field_dlpack_canonical(capsule, layout):
144144

145145
_DLPACK_SUPPORTED_DTYPES = frozenset({f32, f64, i32, i64, u1})
146146

147+
# Cached flag: True when Metal is active with separate command queues (sync needed at interop points).
148+
# Set by _recompute_metal_interop_sync() after qd.init(); cleared by impl.reset() via _clear_metal_interop_cache().
149+
_metal_needs_interop_sync_cached: bool | None = None
150+
151+
152+
def _recompute_metal_interop_sync() -> None:
153+
"""Recompute and cache the Metal interop sync flag from the current config."""
154+
global _metal_needs_interop_sync_cached
155+
cfg = impl.current_cfg()
156+
_metal_needs_interop_sync_cached = cfg.arch == _ARCH_METAL and not cfg.external_metal_command_queue
157+
158+
159+
def _clear_metal_interop_cache() -> None:
160+
"""Invalidate the cached flag. Registered as a reset hook."""
161+
global _metal_needs_interop_sync_cached
162+
_metal_needs_interop_sync_cached = None
163+
164+
165+
_metal_interop_hook_registered = False
166+
167+
168+
def _metal_needs_interop_sync() -> bool:
169+
"""Return True when explicit sync is needed between Quadrants and PyTorch MPS (separate Metal queues)."""
170+
global _metal_interop_hook_registered
171+
if not _metal_interop_hook_registered:
172+
impl.on_reset(_clear_metal_interop_cache)
173+
_metal_interop_hook_registered = True
174+
if _metal_needs_interop_sync_cached is None:
175+
_recompute_metal_interop_sync()
176+
return _metal_needs_interop_sync_cached # type: ignore[return-value]
177+
147178

148179
def _compute_torch_mps_supports_dlpack_bytes_offset() -> bool:
149180
try:
@@ -218,7 +249,7 @@ def _try_zerocopy_torch(field: "Field", *, copy, device=None, is_scalar: bool =
218249
tc = torch.utils.dlpack.from_dlpack(field.to_dlpack())
219250
except RuntimeError as e:
220251
raise ValueError(f"Zero-copy not available: {e}") from None
221-
if impl.current_cfg().arch == _ARCH_METAL and not impl.current_cfg().external_metal_command_queue:
252+
if _metal_needs_interop_sync():
222253
impl.get_runtime().sync()
223254

224255
if device is not None:
@@ -245,7 +276,7 @@ def _mps_sync_if_metal():
245276
When a shared command queue is configured (``external_metal_command_queue != 0``), Metal's sequential command buffer
246277
semantics guarantee ordering automatically and no sync is needed.
247278
"""
248-
if impl.current_cfg().arch == _ARCH_METAL and not impl.current_cfg().external_metal_command_queue:
279+
if _metal_needs_interop_sync():
249280
import torch # pylint: disable=C0415
250281

251282
torch.mps.synchronize()

python/quadrants/lang/impl.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,15 @@ def get_runtime() -> PyQuadrants:
586586
return pyquadrants
587587

588588

589+
_reset_hooks: list[callable] = []
590+
591+
592+
def on_reset(hook: callable) -> None:
593+
"""Register a callback to be invoked on ``reset()``. Subscribers use this
594+
to invalidate module-level caches without ``impl.py`` knowing about them."""
595+
_reset_hooks.append(hook)
596+
597+
589598
def reset():
590599
global pyquadrants
591600
old_ndarrays = pyquadrants.ndarrays
@@ -601,6 +610,9 @@ def reset():
601610

602611
_frozen_dc_plans.clear()
603612

613+
for hook in _reset_hooks:
614+
hook()
615+
604616

605617
@quadrants_scope
606618
def static_print(*args, __p=print, **kwargs):

0 commit comments

Comments
 (0)