iOS ML Support Guide

Copy Markdown View Source

This guide covers adding machine learning capabilities to Dala apps on iOS using the Nx ecosystem.

Overview

For iOS development, these ML backends are supported:

ComponentStatusNotes
Nx✅ ReadyPure Elixir, works on any platform
Axon✅ ReadyNeural networks, pure Elixir
Scholar✅ ReadyTraditional ML (regression, clustering, SVM)
NxSignal✅ ReadyDigital signal processing
EMLX✅ Zero-configMLX backend — recommended for iOS
CoreML✅ ReadyApple Neural Engine, iOS-native
ONNX Runtime⚠️ PlaceholderCross-platform, structure ready

Not supported on iOS: Emily (macOS-only), NxIREE, EXLA/XLA, Torchx.

Quick Start

Dala.ML.setup/0 auto-configures everything based on platform:

defmodule MyApp.App do
  use Dala.App

  def start(_type, _args) do
    # Auto-configures based on platform:
    # - iOS device: EMLX with Metal GPU, JIT disabled (W^X policy)
    # - iOS simulator: EMLX with Metal GPU, JIT enabled
    # - Android: Nx.BinaryBackend
    # - Other: Nx.BinaryBackend
    Dala.ML.setup()

    # ... rest of your app startup
  end
end

No manual config :nx, ... or config :emlx, ... needed!

2. Verify Setup

# Check ML stack status
Dala.ML.status()
# %{platform: :ios_device, backend: {EMLX.Backend, [device: :gpu]}, ...}

# Quick verification
Dala.ML.verify()
# %{status: :ok, sum: 6.0, backend: {EMLX.Backend, [device: :gpu]}}

# Available backends
Dala.ML.available_backends()
# [:nx, :emlx, :coreml, :onnx]

# Benchmark
Dala.ML.benchmark(size: 100, iterations: 10)
# %{time_ms: 1.234, gflops: 0.857, ...}

3. Use ML Libraries

# Nx tensors (auto-configured backend)
tensor = Nx.tensor([1.0, 2.0, 3.0])
Nx.sum(tensor)

# Axon neural networks
model = Axon.input("input", shape: {nil, 784})
       |> Axon.dense(128, activation: :relu)
       |> Axon.dense(10, activation: :softmax)

# Scholar traditional ML
model = Scholar.LinearRegression.fit(features, targets)

# NxSignal DSP
filtered = NxSignal.butterworth(signal, cutoff: 0.2)

EMLX on iOS

Zero-Config Auto-Configuration

Dala.ML.EMLX.setup/0 handles all platform-specific configuration automatically:

PlatformGPUJITNotes
iOS deviceMetal (:gpu)DisabledW^X policy blocks JIT
iOS simulatorMetal (:gpu)EnabledShares Mac's network stack
Non-iOSNx.BinaryBackendN/AFalls back to pure Elixir

Call it once at app startup — no other config needed.

Key Considerations

  1. JIT Compilation: iOS devices enforce W^X (Write XOR Execute) memory protection. JIT compilation is blocked on real devices. Dala.ML.EMLX.setup/0 sets LIBMLX_ENABLE_JIT=false automatically for devices.

  2. iOS Simulator: JIT works in the simulator. Dala.ML.EMLX.setup/0 enables it automatically.

  3. Metal GPU: EMLX uses MLX which leverages Apple's Metal framework. The unified memory architecture of Apple Silicon makes CPU↔GPU data transfer essentially free.

  4. 64-bit Floats: Metal doesn't support 64-bit floats. MLX and EMLX use 32-bit floats.

Device vs Simulator

# Check if running on iOS device or simulator
Dala.ML.EMLX.ios_device?()    # true for real device
Dala.ML.EMLX.ios_simulator?() # true for simulator

# Get platform-appropriate config
Dala.ML.EMLX.platform_config()
# Returns %{device: :gpu, jit_enabled: false, metal_jit: false} for device

Default Device

# Returns the default EMLX device
Dala.ML.EMLX.default_device()
# :gpu on iOS, :cpu on other platforms

CoreML on iOS

Loading and Running Models

# Load a CoreML model
:ok = Dala.ML.CoreML.load_model("/path/to/model.mlmodel", "my_model")

# Check if loaded
true = Dala.ML.CoreML.loaded?("my_model")

# Make prediction (synchronous, runs on dirty CPU scheduler)
{:ok, result_json} = Dala.ML.CoreML.predict("my_model", %{
  "input1" => 1.0,
  "input2" => [1.0, 2.0, 3.0]
})

# Parse result
result = JSON.decode!(result_json)

# Unload when done
:ok = Dala.ML.CoreML.unload_model("my_model")

Converting Models to CoreML

  1. Axon → ONNX → CoreML:

    # Train with Axon
    model = Axon.input("input", shape: {nil, 784}) |> Axon.dense(10, activation: :softmax)
    {init_fn, predict_fn} = Axon.build(model)
    params = init_fn.(Nx.template({1, 784}, :f32), %{})
    
    # Export to ONNX (requires ortonx or onnx package)
    # Then convert ONNX to CoreML using Apple's coremltools (Python)
  2. Use pre-trained CoreML models from Apple or third parties.

Input Types

CoreML supports these input types in the inputs map:

TypeExampleCoreML Mapping
Number1.0MLFeatureValue(double:)
String"hello"MLFeatureValue(string:)
List[1.0, 2.0, 3.0]MLMultiArray
Binary<<...>>MLFeatureValue(data:)

ONNX Runtime (Cross-Platform)

Status

ONNX Runtime integration is currently a placeholder. The Rust NIF layer and dala_onnx crate have correct structure and thread-safe session management, but actual ONNX Runtime linking and inference is not yet implemented.

Setup (When Available)

# Download ONNX Runtime for iOS
cd native/onnxruntime-ios/
# See native/ONNX_RUNTIME_SETUP.md for download instructions

Usage (When Available)

# Create session from ONNX model data
{:ok, session_id} = Dala.ML.ONNX.create_session(model_data)

# Or load from file
{:ok, session_id} = Dala.ML.ONNX.load_model_from_file("model.onnx")

# Run inference
{:ok, output} = Dala.ML.ONNX.run(session_id, input_binary)

# Clean up
:ok = Dala.ML.ONNX.destroy_session(session_id)

Unified API

Dala.ML.predict/2 dispatches to the right backend based on model type:

# CoreML model (string identifier on iOS)
Dala.ML.predict("my_model", %{"input" => [1.0, 2.0, 3.0]})

# ONNX session (integer session ID)
Dala.ML.predict(session_id, input_binary)

# Axon model ({model, params} tuple)
Dala.ML.predict({axon_model, params}, input_tensor)

Example: Simple Neural Network

defmodule MyApp.Model do
  require Axon

  def create_model do
    Axon.input("input", shape: {nil, 784})
    |> Axon.dense(128, activation: :relu)
    |> Axon.dropout(rate: 0.5)
    |> Axon.dense(10, activation: :softmax)
  end

  def train(model, data, labels) do
    model
    |> Axon.Loop.trainer(:categorical_cross_entropy, Axon.Optimizers.adam(0.001))
    |> Axon.Loop.run(data, labels, epochs: 10)
  end
end

Nx Integration

Dala.Ml.Nx provides helpers for backend selection and inference:

# Initialize Nx with the best available backend
Dala.Ml.Nx.init()

# Create a tensor with the default backend
tensor = Dala.Ml.Nx.tensor([1.0, 2.0, 3.0])

# Run inference with an Axon model
{:ok, output} = Dala.Ml.Nx.inference(model, params, input_tensor)

# Check if Axon is available
Dala.Ml.Nx.axon_available?()

Backend Priority

  1. EMLX (if available) — best for Apple Silicon
  2. Nx.BinaryBackend — pure Elixir fallback

Building for iOS

Native Build with EMLX

EMLX requires the MLX library. The build process:

  1. For iOS Simulator: Standard mix dala.deploy --native should work.

  2. For iOS Device: Cross-compile MLX for iOS arm64:

    • Download precompiled MLX iOS binaries from mlx-build
    • Or build from source with iOS SDK
  3. Disable JIT in OTP: Ensure your OTP build has --disable-jit flag.

Environment Variables

# Disable JIT for iOS device builds
export LIBMLX_ENABLE_JIT=false

# Use specific MLX version
export LIBMLX_VERSION=0.31.2

# Cache directory for downloaded binaries
export LIBMLX_CACHE=~/.cache/libmlx

Dependencies

The following dependencies are included in mix.exs for ML support:

DependencyVersionPurpose
:nx~> 0.10Core tensor library
:polaris~> 0.1Nx compiler
:scholar~> 0.4.0Traditional ML algorithms
:nx_signal~> 0.3.0Digital signal processing
:axon~> 0.8.0Neural network library

Limitations

  1. 64-bit float operations: Not supported by Metal. Use 32-bit floats.

  2. Model training: While possible, training large models on-device is limited by memory and compute. Consider:

    • Training in the cloud, deploying to device
    • Using pre-trained models
    • Quantization for smaller models (EMLX supports 4-bit quantization)
  3. ONNX Runtime: Currently placeholder only. Real inference requires linking the ONNX Runtime C library.

Troubleshooting

"JIT not allowed" errors

Ensure LIBMLX_ENABLE_JIT=false and OTP is built with --disable-jit. Dala.ML.EMLX.setup/0 handles this automatically.

"MLX not found" errors

Check that MLX binaries are available for iOS arm64. You may need to:

  1. Set LIBMLX_BUILD=true to build from source
  2. Or provide precompiled binaries via MLX_ARCHIVE_PATH

Memory issues

Use EMLX.clear_cache/0 and EMLX.set_memory_limit/1 to manage GPU memory.

CoreML returns :not_supported

  • CoreML is only available on iOS
  • Ensure model file exists at the specified path
  • Check model format (.mlmodel or .mlpackage)

ONNX available?/0 returns false

  • ONNX Runtime NIF must be compiled for the target platform
  • Verify ONNX Runtime libraries are in native/onnxruntime-ios/

See Also