flyteplugins-omlx 0.0.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flyteplugins/__init__.py +0 -0
- flyteplugins/omlx/__init__.py +3 -0
- flyteplugins/omlx/_app_environment.py +228 -0
- flyteplugins/omlx/_cli.py +79 -0
- flyteplugins/omlx/_constants.py +9 -0
- flyteplugins_omlx-0.0.0a0.dist-info/METADATA +151 -0
- flyteplugins_omlx-0.0.0a0.dist-info/RECORD +10 -0
- flyteplugins_omlx-0.0.0a0.dist-info/WHEEL +5 -0
- flyteplugins_omlx-0.0.0a0.dist-info/entry_points.txt +2 -0
- flyteplugins_omlx-0.0.0a0.dist-info/top_level.txt +1 -0
flyteplugins/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
from dataclasses import dataclass, field, replace
|
|
5
|
+
from typing import Any, Literal, Optional, Union
|
|
6
|
+
|
|
7
|
+
import flyte.app
|
|
8
|
+
import rich.repr
|
|
9
|
+
from flyte import Environment, Image, Resources, SecretRequest
|
|
10
|
+
from flyte.app._types import Port
|
|
11
|
+
from flyte.models import SerializationContext
|
|
12
|
+
|
|
13
|
+
from flyteplugins.omlx._constants import DEFAULT_OMLX_PORT
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _shell_safe(args: list[str]) -> list[str]:
|
|
17
|
+
"""Quote args that have to survive a trip through a shell.
|
|
18
|
+
|
|
19
|
+
`fserve` runs the app with `Popen(" ".join(args), shell=True)` (flyte/_bin/serve.py),
|
|
20
|
+
so any token carrying spaces or quotes -- a `--model-dir` under a user name with a
|
|
21
|
+
space, say -- reaches the server mangled unless it is quoted here. `shlex.quote` is
|
|
22
|
+
the identity function for ordinary tokens, so this is a no-op for everything else.
|
|
23
|
+
|
|
24
|
+
Tokens starting with `$` are left alone: `fserve` expands those against the
|
|
25
|
+
environment *before* joining, and quoting would turn the marker into a literal.
|
|
26
|
+
"""
|
|
27
|
+
return [arg if arg.startswith("$") else shlex.quote(arg) for arg in args]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# oMLX runs only on macOS with Apple Silicon: `mlx` has no Linux wheels, so this default
|
|
31
|
+
# image only installs the plugin. The `omlx` binary itself is expected on the Mac's host
|
|
32
|
+
# (Homebrew or pip-from-git), where the devbox's Apple Silicon bridge runs the app; the
|
|
33
|
+
# `flyte-omlx` wrapper resolves it (probing Homebrew paths that host-execution's PATH
|
|
34
|
+
# omits). See the plugin README.
|
|
35
|
+
DEFAULT_OMLX_IMAGE = flyte.Image.from_debian_base(name="omlx-app-image").with_pip_packages(
|
|
36
|
+
"flyteplugins-omlx",
|
|
37
|
+
pre=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@rich.repr.auto
|
|
42
|
+
@dataclass(kw_only=True, repr=True)
|
|
43
|
+
class OMLXAppEnvironment(flyte.app.AppEnvironment):
|
|
44
|
+
"""
|
|
45
|
+
App environment backed by [oMLX](https://github.com/jundot/omlx) for serving
|
|
46
|
+
LLMs on Apple Silicon Macs.
|
|
47
|
+
|
|
48
|
+
oMLX is an OpenAI-compatible inference server built on Apple's MLX framework.
|
|
49
|
+
It is **macOS + Apple Silicon only**. On a flyte devbox started with
|
|
50
|
+
`--apple-silicon`, this environment requests `gpu="AppleSilicon"` by default, so
|
|
51
|
+
the devbox's bridge runs `omlx serve` natively on the Mac (in an environment built
|
|
52
|
+
from the image's pip packages) and forwards the app's URL, port and logs.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
flyte start devbox --apple-silicon
|
|
56
|
+
flyte create config --devbox
|
|
57
|
+
python examples/accelerators/omlx_serve.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The user is responsible for installing `omlx` on the host machine:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
brew tap jundot/omlx https://github.com/jundot/omlx && brew install omlx
|
|
64
|
+
# or: pip install git+https://github.com/jundot/omlx
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
name: The name of the application.
|
|
69
|
+
image: Container image. oMLX cannot run inside Linux containers, so this
|
|
70
|
+
mostly describes the pip *packages* that get rebuilt into the Mac's host
|
|
71
|
+
environment. Leave as default for the devbox workflow; you typically only
|
|
72
|
+
override it to add pip packages (e.g. `with_pip_packages("omlx @ git+...")`).
|
|
73
|
+
port: Port the oMLX server listens on. Defaults to 8000 (oMLX's own default).
|
|
74
|
+
resources: Compute resources. Defaults to `gpu="AppleSilicon"` so the app runs
|
|
75
|
+
on the Mac's GPU via the devbox bridge; other GPU types are rejected.
|
|
76
|
+
secrets: Secrets requested by the app.
|
|
77
|
+
env_vars: Environment variables for the server process.
|
|
78
|
+
scaling: Scaling configuration.
|
|
79
|
+
domain: Custom domain for the deployed app.
|
|
80
|
+
cluster_pool: Target cluster pool.
|
|
81
|
+
requires_auth: Whether the public URL requires Flyte auth.
|
|
82
|
+
type: App type tag.
|
|
83
|
+
model_dir: Local directory of MLX models, passed to `omlx serve --model-dir`.
|
|
84
|
+
Each subdirectory is exposed as a separate `model_id`. If unset, oMLX falls
|
|
85
|
+
back to `~/.omlx/models`.
|
|
86
|
+
model_id: Optional friendly id surfaced in the Flyte UI and usable as the
|
|
87
|
+
`model` in OpenAI client requests. oMLX derives actual served ids from
|
|
88
|
+
`--model-dir` subdirectories or HuggingFace repo ids.
|
|
89
|
+
api_key: Optional API key for the oMLX server (sent through `--api-key`). For
|
|
90
|
+
real secrets, prefer `secrets` + `env_vars`.
|
|
91
|
+
extra_args: Extra CLI args appended to `omlx serve`. See `omlx serve --help`
|
|
92
|
+
for the full list (`--max-model-memory`, `--max-concurrent-requests`,
|
|
93
|
+
`--paged-ssd-cache-dir`, `--hf-endpoint`, `--log-level`, etc.).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
port: int | Port = DEFAULT_OMLX_PORT
|
|
97
|
+
type: str = "oMLX"
|
|
98
|
+
extra_args: str | list[str] = ""
|
|
99
|
+
model_dir: str = ""
|
|
100
|
+
model_id: str = ""
|
|
101
|
+
api_key: str = ""
|
|
102
|
+
image: str | Image | Literal["auto"] = DEFAULT_OMLX_IMAGE
|
|
103
|
+
_resolved_args: list[str] = field(default_factory=list, init=False, repr=False)
|
|
104
|
+
|
|
105
|
+
def __post_init__(self):
|
|
106
|
+
if self.env_vars is None:
|
|
107
|
+
self.env_vars = {}
|
|
108
|
+
|
|
109
|
+
if self._server is not None:
|
|
110
|
+
raise ValueError("server function cannot be set for OMLXAppEnvironment")
|
|
111
|
+
|
|
112
|
+
if self._on_startup is not None:
|
|
113
|
+
raise ValueError("on_startup function cannot be set for OMLXAppEnvironment")
|
|
114
|
+
|
|
115
|
+
if self._on_shutdown is not None:
|
|
116
|
+
raise ValueError("on_shutdown function cannot be set for OMLXAppEnvironment")
|
|
117
|
+
|
|
118
|
+
if self.args:
|
|
119
|
+
raise ValueError("args cannot be set for OMLXAppEnvironment. Use `extra_args` to add extra arguments.")
|
|
120
|
+
|
|
121
|
+
# oMLX is mac-only: default to running natively on the Mac's GPU through the
|
|
122
|
+
# devbox's Apple Silicon bridge, and refuse any other accelerator.
|
|
123
|
+
device = self.resources.get_device() if self.resources is not None else None
|
|
124
|
+
if device is None:
|
|
125
|
+
if self.resources is None:
|
|
126
|
+
self.resources = Resources(gpu="AppleSilicon")
|
|
127
|
+
else:
|
|
128
|
+
self.resources = replace(self.resources, gpu="AppleSilicon")
|
|
129
|
+
elif device.device_class != "APPLE_SILICON":
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"OMLXAppEnvironment only runs on Apple Silicon (gpu='AppleSilicon') because oMLX uses MLX, "
|
|
132
|
+
"which is darwin/arm64 only; got resources with a non-Apple Silicon device."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if isinstance(self.extra_args, str):
|
|
136
|
+
extra_args = shlex.split(self.extra_args)
|
|
137
|
+
else:
|
|
138
|
+
extra_args = list(self.extra_args)
|
|
139
|
+
|
|
140
|
+
# oMLX is served locally on the Mac (devbox ports are bound to 127.0.0.1), so bind
|
|
141
|
+
# loopback by default; a --host in extra_args wins.
|
|
142
|
+
host_args = [] if any(a == "--host" for a in extra_args) else ["--host", "127.0.0.1"]
|
|
143
|
+
|
|
144
|
+
# We invoke the `flyte-omlx` wrapper (defined in this package) instead of `omlx`
|
|
145
|
+
# directly so the user gets an actionable install hint if oMLX is missing on the
|
|
146
|
+
# host. When oMLX is installed, the wrapper `execv`s straight into it.
|
|
147
|
+
cmd: list[str] = [
|
|
148
|
+
"flyte-omlx",
|
|
149
|
+
"serve",
|
|
150
|
+
*host_args,
|
|
151
|
+
"--port",
|
|
152
|
+
str(self.get_port().port),
|
|
153
|
+
]
|
|
154
|
+
if self.model_dir:
|
|
155
|
+
cmd += ["--model-dir", self.model_dir]
|
|
156
|
+
if self.api_key:
|
|
157
|
+
cmd += ["--api-key", self.api_key]
|
|
158
|
+
cmd += extra_args
|
|
159
|
+
|
|
160
|
+
self.args = _shell_safe(cmd)
|
|
161
|
+
self._resolved_args = cmd
|
|
162
|
+
|
|
163
|
+
if self.image is None or self.image == "auto":
|
|
164
|
+
self.image = DEFAULT_OMLX_IMAGE
|
|
165
|
+
|
|
166
|
+
# Helpful default link to the OpenAI-compatible models endpoint so users can
|
|
167
|
+
# sanity-check the server from the Flyte UI.
|
|
168
|
+
self.links = [flyte.app.Link(path="/v1/models", title="oMLX Models", is_relative=True), *self.links]
|
|
169
|
+
|
|
170
|
+
super().__post_init__()
|
|
171
|
+
|
|
172
|
+
def container_args(self, serialization_context: SerializationContext) -> list[str]:
|
|
173
|
+
"""Return the container arguments for oMLX."""
|
|
174
|
+
if isinstance(self.args, str):
|
|
175
|
+
return shlex.split(self.args)
|
|
176
|
+
return self.args or []
|
|
177
|
+
|
|
178
|
+
def clone_with(
|
|
179
|
+
self,
|
|
180
|
+
name: str,
|
|
181
|
+
image: Optional[Union[str, Image, Literal["auto"]]] = None,
|
|
182
|
+
resources: Optional[Resources] = None,
|
|
183
|
+
env_vars: Optional[dict[str, str]] = None,
|
|
184
|
+
secrets: Optional[SecretRequest] = None,
|
|
185
|
+
depends_on: Optional[list[Environment]] = None,
|
|
186
|
+
description: Optional[str] = None,
|
|
187
|
+
interruptible: Optional[bool] = None,
|
|
188
|
+
**kwargs: Any,
|
|
189
|
+
) -> OMLXAppEnvironment:
|
|
190
|
+
port = kwargs.pop("port", None)
|
|
191
|
+
extra_args = kwargs.pop("extra_args", None)
|
|
192
|
+
model_dir = kwargs.pop("model_dir", None)
|
|
193
|
+
model_id = kwargs.pop("model_id", None)
|
|
194
|
+
api_key = kwargs.pop("api_key", None)
|
|
195
|
+
|
|
196
|
+
if kwargs:
|
|
197
|
+
raise TypeError(f"Unexpected keyword arguments: {list(kwargs.keys())}")
|
|
198
|
+
|
|
199
|
+
kwargs = self._get_kwargs()
|
|
200
|
+
kwargs["name"] = name
|
|
201
|
+
# Reset args so __post_init__ rebuilds them with the new fields.
|
|
202
|
+
kwargs["args"] = None
|
|
203
|
+
kwargs["parameters"] = None
|
|
204
|
+
if image is not None:
|
|
205
|
+
kwargs["image"] = image
|
|
206
|
+
if resources is not None:
|
|
207
|
+
kwargs["resources"] = resources
|
|
208
|
+
if env_vars is not None:
|
|
209
|
+
kwargs["env_vars"] = env_vars
|
|
210
|
+
if secrets is not None:
|
|
211
|
+
kwargs["secrets"] = secrets
|
|
212
|
+
if depends_on is not None:
|
|
213
|
+
kwargs["depends_on"] = depends_on
|
|
214
|
+
if description is not None:
|
|
215
|
+
kwargs["description"] = description
|
|
216
|
+
if interruptible is not None:
|
|
217
|
+
kwargs["interruptible"] = interruptible
|
|
218
|
+
if port is not None:
|
|
219
|
+
kwargs["port"] = port
|
|
220
|
+
if extra_args is not None:
|
|
221
|
+
kwargs["extra_args"] = extra_args
|
|
222
|
+
if model_dir is not None:
|
|
223
|
+
kwargs["model_dir"] = model_dir
|
|
224
|
+
if model_id is not None:
|
|
225
|
+
kwargs["model_id"] = model_id
|
|
226
|
+
if api_key is not None:
|
|
227
|
+
kwargs["api_key"] = api_key
|
|
228
|
+
return replace(self, **kwargs)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Friendly preflight wrapper around the upstream `omlx` console command.
|
|
2
|
+
|
|
3
|
+
An `OMLXAppEnvironment` serves via `omlx serve`. The local-serve / Apple Silicon
|
|
4
|
+
host process invokes `flyte-omlx` instead of `omlx` directly so that, if oMLX isn't
|
|
5
|
+
installed on the Mac, the user sees a clear install hint instead of a raw
|
|
6
|
+
`FileNotFoundError: 'omlx'`.
|
|
7
|
+
|
|
8
|
+
When oMLX *is* installed, this wrapper is transparent: it `execv`'s into the real
|
|
9
|
+
binary (or imports `omlx.cli` if the package is installed but the console script
|
|
10
|
+
isn't on PATH for some reason).
|
|
11
|
+
|
|
12
|
+
`gpu="AppleSilicon"` workloads on the flyte devbox run their app natively on the Mac
|
|
13
|
+
in a `uv` environment whose `PATH` only contains that environment's bin directory. A
|
|
14
|
+
Homebrew-installed oMLX lives at `/opt/homebrew/bin` (Apple Silicon) or
|
|
15
|
+
`/usr/local/bin` (Intel Macs and older setups), neither of which is on that `PATH`, so
|
|
16
|
+
`shutil.which("omlx")` alone would miss it. We probe those locations explicitly.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
_INSTALL_HINT = """\
|
|
26
|
+
[flyteplugins-omlx] oMLX is not installed on this machine.
|
|
27
|
+
|
|
28
|
+
oMLX is macOS + Apple Silicon only and is not published to PyPI. Install it
|
|
29
|
+
with one of:
|
|
30
|
+
|
|
31
|
+
brew tap jundot/omlx https://github.com/jundot/omlx && brew install omlx
|
|
32
|
+
pip install git+https://github.com/jundot/omlx
|
|
33
|
+
|
|
34
|
+
Or download the .dmg from https://github.com/jundot/omlx/releases.
|
|
35
|
+
|
|
36
|
+
Then re-run `flyte serve` / redeploy the app. See the plugin README for details.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Common Homebrew prefixes for the `omlx` binary. Checked in `resolve_omlx` after
|
|
40
|
+
# `shutil.which`, because the Apple Silicon host-execution environment's PATH
|
|
41
|
+
# omits Homebrew.
|
|
42
|
+
_HOMEBREW_OMLX_CANDIDATES = ("/opt/homebrew/bin/omlx", "/usr/local/bin/omlx")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_omlx() -> str | None:
|
|
46
|
+
"""The path to the `omlx` binary, or None if it cannot be found anywhere."""
|
|
47
|
+
found = shutil.which("omlx")
|
|
48
|
+
if found:
|
|
49
|
+
return found
|
|
50
|
+
for candidate in _HOMEBREW_OMLX_CANDIDATES:
|
|
51
|
+
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
|
52
|
+
return candidate
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> int:
|
|
57
|
+
binary = resolve_omlx()
|
|
58
|
+
if binary:
|
|
59
|
+
# Hand off to the real binary, preserving argv. execv replaces this
|
|
60
|
+
# process, so the real omlx becomes the long-running server and its
|
|
61
|
+
# signals/log streams belong to it.
|
|
62
|
+
os.execv(binary, [binary, *sys.argv[1:]])
|
|
63
|
+
return 0 # not reached
|
|
64
|
+
|
|
65
|
+
# Fall back to the installed Python package, in case the script wasn't
|
|
66
|
+
# placed on PATH (e.g. an unusual install layout).
|
|
67
|
+
try:
|
|
68
|
+
import omlx.cli as omlx_cli # type: ignore[import-not-found]
|
|
69
|
+
except ImportError:
|
|
70
|
+
sys.stderr.write(_INSTALL_HINT)
|
|
71
|
+
return 127
|
|
72
|
+
|
|
73
|
+
sys.argv = ["omlx", *sys.argv[1:]]
|
|
74
|
+
rc = omlx_cli.main()
|
|
75
|
+
return int(rc) if rc is not None else 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
sys.exit(main())
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Constants shared by the oMLX plugin."""
|
|
2
|
+
|
|
3
|
+
OMLX_MIN_VERSION = (0, 1, 0)
|
|
4
|
+
OMLX_MIN_VERSION_STR = ".".join(map(str, OMLX_MIN_VERSION))
|
|
5
|
+
|
|
6
|
+
# oMLX's own default serve port. Kept distinct from the SDK's 8080 so that
|
|
7
|
+
# "just construct an OMLXAppEnvironment" matches what `omlx serve` would bind
|
|
8
|
+
# if you ran it by hand.
|
|
9
|
+
DEFAULT_OMLX_PORT = 8000
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flyteplugins-omlx
|
|
3
|
+
Version: 0.0.0a0
|
|
4
|
+
Summary: oMLX plugin for flyte — serve LLMs on Apple Silicon
|
|
5
|
+
Author: Flyte authors
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: flyte>=2.0.0b43
|
|
9
|
+
|
|
10
|
+
# Flyte oMLX Plugin
|
|
11
|
+
|
|
12
|
+
Serve LLMs locally on Apple Silicon using [oMLX](https://github.com/jundot/omlx)
|
|
13
|
+
and Flyte Apps.
|
|
14
|
+
|
|
15
|
+
This plugin provides the `OMLXAppEnvironment` class for defining an oMLX-backed
|
|
16
|
+
app environment. oMLX is an OpenAI-compatible inference server built on Apple's
|
|
17
|
+
MLX framework, macOS + Apple Silicon only.
|
|
18
|
+
|
|
19
|
+
The plugin integrates with the **Apple Silicon flyte devbox**: an
|
|
20
|
+
`OMLXAppEnvironment` requests `gpu="AppleSilicon"` by default, so the devbox's
|
|
21
|
+
bridge runs `omlx serve` natively on the Mac, in an environment built from the
|
|
22
|
+
image's pip packages, and forwards the app's URL, port and logs.
|
|
23
|
+
|
|
24
|
+
> **Mac-only.** oMLX runs only on macOS 15+ with Apple Silicon (M1/M2/M3/M4).
|
|
25
|
+
> `mlx` has no Linux wheels, so this plugin cannot run in a Linux container or on
|
|
26
|
+
> a cloud cluster. It is designed for a devbox started with `--apple-silicon`.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
oMLX is **not on PyPI** — install it via Homebrew, the macOS `.dmg`, or from
|
|
31
|
+
source. See the upstream
|
|
32
|
+
[install guide](https://github.com/jundot/omlx#installation):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# oMLX itself (one of):
|
|
36
|
+
brew tap jundot/omlx https://github.com/jundot/omlx && brew install omlx
|
|
37
|
+
# or:
|
|
38
|
+
pip install git+https://github.com/jundot/omlx
|
|
39
|
+
|
|
40
|
+
# Then the Flyte plugin:
|
|
41
|
+
pip install --pre flyteplugins-omlx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Verify the install with `omlx --help` before running the plugin.
|
|
45
|
+
|
|
46
|
+
## Quickstart — serve an LLM from the Apple Silicon devbox
|
|
47
|
+
|
|
48
|
+
`examples/accelerators/omlx_serve.py`:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import flyte
|
|
52
|
+
from flyteplugins.omlx import OMLXAppEnvironment
|
|
53
|
+
|
|
54
|
+
omlx_app = OMLXAppEnvironment(
|
|
55
|
+
name="omlx-local",
|
|
56
|
+
# oMLX serves whatever is under ~/.omlx/models by default. Set model_dir to a
|
|
57
|
+
# different folder (an absolute, expanded path).
|
|
58
|
+
model_id="qwen3-0.6b",
|
|
59
|
+
extra_args="--log-level info",
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Run it against a devbox started with the Apple Silicon bridge:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
flyte start devbox --apple-silicon
|
|
67
|
+
flyte create config --devbox
|
|
68
|
+
python examples/accelerators/omlx_serve.py
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`OMLXAppEnvironment` defaults to `gpu="AppleSilicon"`, so the app runs natively
|
|
72
|
+
on the Mac's GPU. From another shell, hit the OpenAI-compatible API (the app's
|
|
73
|
+
URL is printed by the example):
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from openai import OpenAI
|
|
77
|
+
|
|
78
|
+
client = OpenAI(base_url="http://omlx-local-...:30081/v1", api_key="not-used")
|
|
79
|
+
resp = client.chat.completions.create(
|
|
80
|
+
model="qwen3-0.6b",
|
|
81
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
82
|
+
)
|
|
83
|
+
print(resp.choices[0].message.content)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Useful endpoints (also surfaced as Flyte app links):
|
|
87
|
+
|
|
88
|
+
| Endpoint | Description |
|
|
89
|
+
|--------------------|----------------------------------------------|
|
|
90
|
+
| `/v1/chat/completions` | OpenAI chat completions |
|
|
91
|
+
| `/v1/completions` | OpenAI completions |
|
|
92
|
+
| `/v1/embeddings` | OpenAI embeddings |
|
|
93
|
+
| `/v1/models` | List available models |
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
`OMLXAppEnvironment` constructs the `omlx serve --host 127.0.0.1 --port <port> ...`
|
|
98
|
+
invocation that the devbox runs on the Mac. Recognised fields:
|
|
99
|
+
|
|
100
|
+
| Field | Description |
|
|
101
|
+
|---------------|---------------------------------------------------------------------------------------------------|
|
|
102
|
+
| `name` | App name (required, lowercase + hyphens). |
|
|
103
|
+
| `port` | Bind port. Default `8000` (oMLX default). |
|
|
104
|
+
| `model_dir` | Local directory containing MLX-format model subdirectories, mapped to `--model-dir`. Use an expanded absolute path. |
|
|
105
|
+
| `model_id` | Friendly id surfaced in Flyte / OpenAI client requests; oMLX serves each subdir of `model_dir`. |
|
|
106
|
+
| `resources` | `gpu="AppleSilicon"` by default (runs on the Mac via the devbox bridge). Other GPUs are rejected. |
|
|
107
|
+
| `api_key` | Optional `--api-key` for the oMLX server. For real secrets, prefer `secrets` + `env_vars`. |
|
|
108
|
+
| `extra_args` | List or string of extra flags passed to `omlx serve`. See `omlx serve --help`. |
|
|
109
|
+
| `env_vars` | Environment variables for the server process (e.g. `HF_TOKEN`). |
|
|
110
|
+
| `image` | Container image. Sets the pip packages rebuilt into the Mac's host environment. |
|
|
111
|
+
| `scaling` | `flyte.app.Scaling` config. |
|
|
112
|
+
|
|
113
|
+
### Example: tighten memory / cache config
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
omlx_app = OMLXAppEnvironment(
|
|
117
|
+
name="omlx-local",
|
|
118
|
+
model_dir="/Users/me/.omlx/models",
|
|
119
|
+
extra_args=(
|
|
120
|
+
"--max-model-memory 24GB "
|
|
121
|
+
"--max-concurrent-requests 4 "
|
|
122
|
+
"--paged-ssd-cache-dir /Users/me/.omlx/ssd_cache "
|
|
123
|
+
"--paged-ssd-cache-max-size 50GB "
|
|
124
|
+
"--log-level info"
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## How it works
|
|
130
|
+
|
|
131
|
+
With `flyte start devbox --apple-silicon`, a `gpu="AppleSilicon"` app runs
|
|
132
|
+
**natively on the Mac**. The devbox container (a Linux VM) has no Metal, so the
|
|
133
|
+
bridge hands the app's entrypoint (`fserve`) to the Mac, which runs it in a `uv`
|
|
134
|
+
environment built from the image's pip packages. `fserve` then runs the app's
|
|
135
|
+
`args` — here `omlx serve ...` — on the Mac, and the devbox forwards the app's
|
|
136
|
+
URL, port and logs.
|
|
137
|
+
|
|
138
|
+
The `args` invoke a tiny `flyte-omlx` console-script wrapper (installed by this
|
|
139
|
+
package) which `execv`s into the real `omlx` binary. Because the Apple Silicon
|
|
140
|
+
host environment's PATH only contains its own `bin/` (Homebrew is not on it), the
|
|
141
|
+
wrapper also probes `/opt/homebrew/bin` and `/usr/local/bin` so a
|
|
142
|
+
Homebrew-installed oMLX is found. If oMLX isn't installed, the wrapper prints an
|
|
143
|
+
actionable install hint instead of a raw `FileNotFoundError`.
|
|
144
|
+
|
|
145
|
+
## Limitations
|
|
146
|
+
|
|
147
|
+
- **Apple Silicon only.** oMLX uses `mlx`, which only ships for darwin/arm64. You
|
|
148
|
+
cannot run this plugin in a Linux container today.
|
|
149
|
+
- oMLX must be installed on the Mac (Homebrew or pip-from-git). The default image
|
|
150
|
+
installs only the plugin, because `mlx` has no Linux wheels with which to build a
|
|
151
|
+
base image.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
flyteplugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
flyteplugins/omlx/__init__.py,sha256=OVNvya0WcAvUOBczbjKmtRpG4nJhAqHgBOGFDfVOWPM,142
|
|
3
|
+
flyteplugins/omlx/_app_environment.py,sha256=B_fK-bsfkHHk4lVD9LxzoNll1jXt23zs6CU4fdESMdk,9843
|
|
4
|
+
flyteplugins/omlx/_cli.py,sha256=Xp1u1qn6EzJZ0q4rlJnYxULLDQD3nINaDk9VMV6Cplg,2881
|
|
5
|
+
flyteplugins/omlx/_constants.py,sha256=ultyT7SMIPHoKC6GweyGRPui2IaknZ8UEgEg3wezvD4,337
|
|
6
|
+
flyteplugins_omlx-0.0.0a0.dist-info/METADATA,sha256=9R_buwtJwiFMHmmkODGc4W6dB4Ehm2-MpAI6w7B5C0I,6335
|
|
7
|
+
flyteplugins_omlx-0.0.0a0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
flyteplugins_omlx-0.0.0a0.dist-info/entry_points.txt,sha256=-oW89sQ9JlQ44d1g-GSG5sNyI7V2yPvzacH_sYXekss,59
|
|
9
|
+
flyteplugins_omlx-0.0.0a0.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
|
|
10
|
+
flyteplugins_omlx-0.0.0a0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flyteplugins
|