Skip to content

Commit 9b5ac0f

Browse files
kkilchristBerkkirikclaude
committed
Support Apple Silicon (MPS): --device auto and CUDA-only Triton gate
Apply upstream PR openai#22 by @Berkkirik: "--device auto" resolves cuda > mps > cpu, and the Triton MoE kernels are auto-enabled only on CUDA devices (Triton cannot target Metal). Verified end-to-end on Apple Silicon (M4 Pro, torch 2.12.1) with the released checkpoint: spans identical to CPU output. Co-authored-by: berkkirik <berk.kirik@outlook.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f7f00ca commit 9b5ac0f

5 files changed

Lines changed: 71 additions & 9 deletions

File tree

opf/_cli/common.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,13 @@ def add_device_arg(parser: object) -> None:
5555
parser.add_argument(
5656
"--device",
5757
type=str,
58-
default="cuda",
59-
help="Device to run on",
58+
default="auto",
59+
help=(
60+
"Device to run on. 'auto' (default) picks the best available "
61+
"backend: cuda > mps (Apple Silicon) > cpu. Pass an explicit "
62+
"value like 'cuda', 'mps', or 'cpu' to override or to get a "
63+
"loud error when the requested backend is unavailable."
64+
),
6065
)
6166

6267

opf/_common/device.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Device-name resolution helpers shared by CLI entrypoints."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
import torch
8+
9+
AUTO_DEVICE: str = "auto"
10+
11+
12+
def _mps_is_available() -> bool:
13+
"""Return True when the current PyTorch build supports Apple Metal (MPS)."""
14+
backend = getattr(torch.backends, "mps", None)
15+
if backend is None:
16+
return False
17+
is_available = getattr(backend, "is_available", None)
18+
if is_available is None:
19+
return False
20+
try:
21+
return bool(is_available())
22+
except Exception:
23+
return False
24+
25+
26+
def resolve_device(device_name: str) -> torch.device:
27+
"""Resolve a user-supplied device name into a concrete ``torch.device``.
28+
29+
``"auto"`` selects the best available device in this order: CUDA (NVIDIA
30+
GPU) > MPS (Apple Silicon GPU) > CPU. Any other value is passed through
31+
to ``torch.device`` as-is so that explicit requests like ``"cuda"`` or
32+
``"mps"`` still fail loudly when the underlying backend is unavailable.
33+
"""
34+
if device_name == AUTO_DEVICE:
35+
if torch.cuda.is_available():
36+
return torch.device("cuda")
37+
if _mps_is_available():
38+
print(
39+
"info: no CUDA device detected; using Apple Metal (MPS).",
40+
file=sys.stderr,
41+
flush=True,
42+
)
43+
return torch.device("mps")
44+
print(
45+
"info: no CUDA or MPS device detected; falling back to CPU "
46+
"(pass --device cuda or --device mps to override).",
47+
file=sys.stderr,
48+
flush=True,
49+
)
50+
return torch.device("cpu")
51+
return torch.device(device_name)

opf/_core/runtime.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
REDACTED_OUTPUT_LABEL,
2020
REDACTED_OUTPUT_PLACEHOLDER,
2121
)
22+
from .._common.device import resolve_device
2223
from .._common.env import get_env_bool
2324
from .decoding import ViterbiCRFDecoder
2425
from .._common.label_space import resolve_label_space_from_config
@@ -215,7 +216,7 @@ def load_inference_runtime(
215216
if output_mode not in OUTPUT_MODES:
216217
raise ValueError(f"Unsupported output_mode: {output_mode!r}")
217218
_validate_checkpoint_dir(checkpoint)
218-
device = torch.device(device_name)
219+
device = resolve_device(device_name)
219220
checkpoint_config = _load_checkpoint_config(checkpoint)
220221
n_ctx = _resolve_n_ctx(checkpoint_config, n_ctx_override, device)
221222
encoding_name = checkpoint_config.get("encoding")

opf/_model/model.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -750,8 +750,11 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
750750
expert_indices = experts.indices
751751
expert_weights = expert_weights / self.experts_per_token
752752
experts_per_token_eff = self.experts_per_token
753-
not_running_on_cpu = t.device.type != "cpu"
754-
use_triton = get_env_bool("OPF_MOE_TRITON", default=not_running_on_cpu)
753+
# Triton kernels are CUDA-only; auto-enable only on CUDA devices. MPS
754+
# and CPU fall back to the torch-ops path unless the user explicitly
755+
# opts in via OPF_MOE_TRITON=1.
756+
is_cuda_device = t.device.type == "cuda"
757+
use_triton = get_env_bool("OPF_MOE_TRITON", default=is_cuda_device)
755758
if use_triton:
756759
_require_triton()
757760

opf/_train/runner.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .args import parse_args
1717
from .._api import resolve_checkpoint_path
1818
from .._common.constants import SCHEMA_VERSION
19+
from .._common.device import resolve_device
1920
from .._common.label_space import (
2021
resolve_checkpoint_label_space,
2122
resolve_label_space_from_config,
@@ -588,11 +589,12 @@ def main(argv: Sequence[str] | None = None, *, prog: str | None = None) -> int:
588589
progress_interval_s = parsed_interval
589590

590591
checkpoint = resolve_checkpoint_path(args.checkpoint)
591-
device = torch.device(args.device)
592+
device = resolve_device(args.device)
592593

593-
# Default to Triton-backed MoE kernels on non-CPU devices unless callers
594-
# explicitly opt out. CPU uses torch ops by default so Triton stays optional.
595-
if device.type != "cpu":
594+
# Default to Triton-backed MoE kernels on CUDA devices unless callers
595+
# explicitly opt out. CPU and MPS use torch ops by default so Triton
596+
# stays CUDA-only (the kernels don't run on Metal).
597+
if device.type == "cuda":
596598
os.environ.setdefault("OPF_MOE_TRITON", "1")
597599

598600
base_config = _load_checkpoint_config(checkpoint)

0 commit comments

Comments
 (0)