Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml → .github/workflows/emlx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
run: sudo apt-get install -y libopenblas0

- name: Compile and check warnings
working-directory: emlx
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"

Expand All @@ -62,6 +63,7 @@ jobs:
epmd -daemon

- name: Run tests
working-directory: emlx
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
if [ "${{ matrix.job.build }}" = "true" ]; then
Expand Down Expand Up @@ -102,6 +104,7 @@ jobs:
echo "${ELIXIR_PATH}" >> $GITHUB_PATH

- name: Setup Mix
working-directory: emlx
run: |
mix local.hex --force
mix local.rebar --force
Expand All @@ -112,16 +115,18 @@ jobs:
uses: actions/cache@v4
id: mix-cache # id to use in retrieve action
with:
path: ${{ github.workspace }}/deps
key: ${{ runner.os }}-Elixir-v${{ matrix.job.elixir }}-OTP-${{ matrix.job.otp }}-${{ hashFiles(format('{0}/mix.lock', github.workspace)) }}-v1
path: ${{ github.workspace }}/emlx/deps
key: ${{ runner.os }}-Elixir-v${{ matrix.job.elixir }}-OTP-${{ matrix.job.otp }}-${{ hashFiles(format('{0}/emlx/mix.lock', github.workspace)) }}-v1

- name: Install dependencies
if: ${{ steps.mix-cache.outputs.cache-hit != 'true' }}
working-directory: emlx
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
mix deps.get

- name: Compile and check warnings
working-directory: emlx
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
if [ "${{ matrix.job.build }}" = "true" ]; then
Expand All @@ -131,6 +136,7 @@ jobs:

- name: Check formatting
if: ${{ matrix.job.lint }}
working-directory: emlx
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
if [ "${{ matrix.job.build }}" = "true" ]; then
Expand All @@ -144,6 +150,7 @@ jobs:
epmd -daemon

- name: Run tests
working-directory: emlx
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
export EMLX_TEST_DEFAULT_GPU="${{ matrix.job.gpu }}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
name: Validation
name: EMLXAxon

on:
push:
branches:
- main
paths:
- 'lib/**'
- 'c_src/**'
- 'validation/**'
- 'mix.exs'
- 'mix.lock'
- 'emlx/lib/**'
- 'emlx/c_src/**'
- 'emlx/mix.exs'
- 'emlx/mix.lock'
- 'emlx_axon/lib/**'
- 'emlx_axon/test/**'
- 'emlx_axon/mix.exs'
- 'emlx_axon/mix.lock'
workflow_dispatch:

concurrency:
group: validation-${{ github.ref }}
group: emlx-axon-${{ github.ref }}
cancel-in-progress: true

jobs:
validation:
name: Validation suite (macOS arm64)
emlx_axon:
name: EMLXAxon suite (macOS arm64)
runs-on: macos-26
env:
MIX_ENV: test
Expand All @@ -45,38 +48,45 @@ jobs:
mix local.hex --force
mix local.rebar --force

- name: Cache root deps and build
- name: Cache emlx deps and build
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/deps
${{ github.workspace }}/_build
key: ${{ runner.os }}-validation-root-${{ hashFiles('mix.lock', 'mix.exs', 'c_src/**') }}-v1
${{ github.workspace }}/emlx/deps
${{ github.workspace }}/emlx/_build
key: ${{ runner.os }}-emlx-axon-emlx-${{ hashFiles('emlx/mix.lock', 'emlx/mix.exs', 'emlx/c_src/**') }}-v1

- name: Cache validation deps
- name: Cache emlx_axon deps and build
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/validation/deps
${{ github.workspace }}/validation/_build
key: ${{ runner.os }}-validation-deps-${{ hashFiles('validation/mix.lock') }}-v1
${{ github.workspace }}/emlx_axon/deps
${{ github.workspace }}/emlx_axon/_build
key: ${{ runner.os }}-emlx-axon-${{ hashFiles('emlx_axon/mix.lock', 'emlx_axon/mix.exs') }}-v1

- name: Build EMLX (root project)
- name: Build EMLX
working-directory: emlx
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
mix deps.get
mix compile

- name: Install validation dependencies
working-directory: validation
- name: Install emlx_axon dependencies
working-directory: emlx_axon
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
mix deps.get

- name: Run validation suite
working-directory: validation
- name: Check formatting
working-directory: emlx_axon
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
mix format --check-formatted

- name: Run emlx_axon tests
working-directory: emlx_axon
env:
EMLX_TEST_DEFAULT_GPU: "true"
run: |
export PATH="${{ steps.setup.outputs.path }}:${PATH}"
mix test --only validation
mix test --exclude quantized_inference --warnings-as-errors
96 changes: 5 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,11 @@
# EMLX

[![Package](https://img.shields.io/badge/-Package-important)](https://hex.pm/packages/emlx) [![Documentation](https://img.shields.io/badge/-Documentation-blueviolet)](https://hexdocs.pm/emlx)
This repository holds the following projects:

EMLX is the Nx Backend for the [MLX](https://github.com/ml-explore/mlx) library.
* [`EMLX`](https://github.com/elixir-nx/emlx/tree/main/emlx#readme) - Nx backend for Apple's [MLX](https://github.com/ml-explore/mlx) library, enabling GPU-accelerated tensor operations on Apple Silicon

Because of MLX's nature, EMLX with GPU backend is only supported on macOS.
* [`EMLXAxon`](https://github.com/elixir-nx/emlx/tree/main/emlx_axon#readme) - [Axon](https://github.com/elixir-nx/axon) model rewrites that swap supported nodes for `EMLX.Fast` Metal shader implementations, accelerating LLM inference on Apple Silicon

MLX with CPU backend is available on most mainstream platforms, however, the CPU backend may not be as optimized as the GPU backend,
especially for non-macOS OSes, as they're not prioritized for development. Right now, EMLX supports x86_64 and arm64 architectures
on both macOS and Linux.
Each has their own README, which you can access above to learn more.

The M-Series Macs have an unified memory architecture, which allows for more passing data between the CPU and GPU to be effectively a no-op.

Besides the backend, this library also provides a `Nx.Defn.Compiler` implementation that JIT-compiles Nx functions with smart use of MLX command queues.

- **Worker-thread dispatch** — MLX ops run on dedicated threads instead of BEAM dirty schedulers, eliminating scheduler starvation under load.
- **Per-process Metal command queues (`EMLX.CommandQueue`)** — each BEAM process can get its own GPU command queue for true process-level GPU isolation.
- **GPU pointer interop** — `Nx.Backend.from_pointer/5` and `to_pointer/2` for zero-copy Metal buffer sharing with other languages, such as with Python via Pythonx.

Metal does not support 64-bit floats, so neither MLX nor EMLX do either.

## Usage

To use EMLX, you can add it as a dependency in your `mix.exs`:

```elixir
def deps do
[
{:emlx, github: "elixir-nx/emlx", branch: "main"}
]
end
```

Then, you just need to set `EMLX.Backend` as the default backend for your Nx functions:

```elixir
Nx.default_backend(EMLX.Backend)

# Setting the device to the CPU (default)
Nx.default_backend({EMLX.Backend, device: :cpu})

# Setting the device to the GPU
Nx.default_backend({EMLX.Backend, device: :gpu})

# or use the application config using one of the alternatives above as the value:

config :nx, :default_backend, EMLX.Backend
config :nx, :default_backend, {EMLX.Backend, device: :cpu}
config :nx, :default_backend, {EMLX.Backend, device: :gpu}
```

If you want to use the JIT compiler, you can set the default compiler as shown below.

```elixir
Nx.Defn.default_options(compiler: EMLX)

# Alternatively, we can set this in the application environment

config :nx, :default_defn_options, compiler: EMLX
```

### MLX binaries

EMLX relies on the [MLX](https://github.com/ml-explore/mlx) library to function, and currently EMLX will download precompiled builds from [mlx-build](https://github.com/cocoa-xu/mlx-build).

#### Using precompiled binaries

While the default configuration should be suitable for most cases, there is however a number of environment variables that you may want to use in order to customize the variant of MLX binary.

The binaries are always downloaded to match the current configuration, so you should set the environment variables in .bash_profile or a similar configuration file so you don't need to export it in every shell session.

##### `LIBMLX_VERSION`

The version of the MLX binary to download. By default EMLX will always use the latest version possible.

##### `LIBMLX_ENABLE_JIT`

Defaults to `false`.

Using JIT compilation for Metal kernels when set to `true`.

##### `LIBMLX_ENABLE_DEBUG`

Defaults to `false`.

Enhance metal debug workflow by enabling debug information in the Metal shaders when set to `true`.

##### `LIBMLX_CACHE`

The directory to store the downloaded and built archives in. Defaults to the standard cache location for the given operating system.

#### Compiling from source

If you want to compile MLX from source, you can do so by setting the `LIBMLX_BUILD` environment variable to `true`.

Environment variables listed in the previous section will still apply.
[Check our organization page for a general introduction to Machine Learning in Elixir](https://github.com/elixir-nx/).
Loading
Loading