← Back to Docs

DSPEX NUMERICAL AND VECTOR 03 exla

Documentation for DSPEX_NUMERICAL_AND_VECTOR_03_exla from the Ds ex repository.

Excellent question. This gets to the heart of the development strategy for high-performance libraries in Elixir.

Yes, EXLA should absolutely be part of your pipeline at some point. It is the key that unlocks the massive performance potential of Nx and allows your Elixir-based AI framework to compete with, and likely exceed, the performance of C++/CUDA-backed Python libraries.

Think of it this way:

  • Nx gives you the correctness and the high-level API (the “what to do”).
  • EXLA gives you the performance (the “how to do it fast”).

The architecture is designed so you can develop the logic first with Nx, and then “plug in” EXLA as a high-performance backend with minimal code changes.


When to Integrate EXLA

You should integrate EXLA at a specific, strategic point in your development lifecycle. Do not do it now. The right time is:

After you have a fully functional and tested vector retrieval module using only Nx.

Here are the specific trigger points that signal it’s time to add EXLA:

  1. Correctness is Confirmed: Your DSPEx.Retrieval.VectorSearch module (or whatever you name it) passes all its unit and integration tests using the default Nx pure Elixir backend. You are 100% confident that the logic is correct.
  2. Performance Becomes the Bottleneck: You run benchmarks on your retrieval module and find that, while correct, it is too slow for practical use. This is expected with the default backend. This proves the need for acceleration.
  3. Before a Major Feature Demo or Release: When you are preparing to showcase the performance advantages of DSPEx, or before a production-ready release, you integrate EXLA to deliver on the promise of high speed.
  4. When You Start Benchmarking Against Python DSPy: To do a fair “apples-to-apples” performance comparison, you need to be running on an optimized backend, just as DSPy runs on numpy’s optimized C/Fortran backend.

In your project roadmap, this would be a distinct phase: “Phase 2: Performance Acceleration.”


How to Integrate EXLA: A Step-by-Step Guide

The integration is surprisingly straightforward, thanks to the design of Nx. It’s more of a configuration and environment setup task than a heavy coding one.

Step 1: Add the EXLA Dependency

In your mix.exs file, add exla to your list of dependencies alongside nx.

# mix.exs
def deps do
  [
    {:nx, "~> 0.9"},
    {:exla, "~> 0.9"}, # Add this line
    # ... other dependencies
  ]
end

Then run mix deps.get.

Step 2: Configure EXLA as the Default Backend

In your config/config.exs, tell Nx to use EXLA for all its computations by default. This is the “magic switch” that globally accelerates your Nx code.

# config/config.exs
import Config

# Configure Nx to use the EXLA backend globally.
# All Nx operations will now be JIT-compiled and run through XLA.
config :nx, :default_backend, EXLA.Backend

What this does: Every time you call an Nx function (like Nx.dot/2 or Nx.exp/1), Nx will now delegate the work to the EXLA backend instead of its pure Elixir one. EXLA will then compile a highly optimized version of that computation for your CPU (or GPU, if configured) and run it.

While Step 2 globally enables EXLA, you can gain even more performance by telling EXLA to compile entire functions, not just individual operations. You do this by wrapping your performance-critical code in a defn block.

Your VectorSearch module is the perfect candidate for this.

Before (Nx only):

# lib/dspex/retrieval/vector_search.ex
defmodule DSPEx.Retrieval.VectorSearch do
  def find_top_k(query_tensor, corpus_tensors, k) do
    # ... logic using Nx.dot, Nx.argsort, etc. ...
  end
end

After (Optimized for EXLA):

# lib/dspex/retrieval/vector_search.ex
defmodule DSPEx.Retrieval.VectorSearch do
  import Nx.Defn # Import the defn macro

  # Wrap the entire performance-critical function in `defn`.
  # The code inside remains IDENTICAL.
  defn find_top_k(query_tensor, corpus_tensors, k) do
    query_norm = Nx.LinAlg.normalize(query_tensor)
    corpus_norm = Nx.LinAlg.normalize(corpus_tensors, axis: 1)
    scores = Nx.dot(query_norm, Nx.transpose(corpus_norm))
    Nx.argsort(scores, direction: :desc) |> Nx.slice_axis(0, k)
  end
end

By doing this, EXLA will compile the entire find_top_k function into a single, fused computational graph, minimizing data transfer and maximizing performance. This is where you’ll see order-of-magnitude speedups.

Step 4: Verify and Benchmark

After making these changes, you must verify that everything is working as expected.

  1. Run your tests again: All of your existing tests for the retrieval module should still pass without any changes.
  2. Run benchmarks: Create a benchmark script (using a library like Benchee) to measure the performance before and after adding EXLA. You should see a dramatic improvement.
  3. Check for GPU/TPU (Optional Advanced Step): To enable GPU acceleration, you don’t change your Elixir code. Instead, you configure your environment. When running your application or tests, you would set environment variables:
    # For NVIDIA GPUs
    export XLA_TARGET=cuda122 # Or your specific CUDA version
    
    # For AMD GPUs
    export XLA_TARGET=rocm
    
    # Then run your command
    mix test
    
    EXLA will automatically detect these settings and compile the code for the GPU.

Strategic Roadmap for Numerical Performance

Here is a clear, phased plan for your project:

PhaseTaskDependenciesOutcome
Phase 1: Logic & Correctness (Current Focus)Implement vector retrieval using the Nx API.{:nx, ...}A functionally correct but slow retrieval module. All tests pass.
Phase 2: Performance Acceleration (Next Step)Add EXLA and configure it as the default backend. Wrap critical functions in defn.{:exla, ...}A dramatically faster retrieval module running on the CPU with no logic changes.
Phase 3: Hardware Acceleration (Advanced)Configure XLA_TARGET environment variables for GPU/TPU support.System dependencies (CUDA/ROCM)An extremely fast retrieval module that leverages available GPU hardware, still with no logic changes.

This phased approach minimizes risk, separates concerns, and allows you to deliver a correct, robust feature first, and then layer on performance optimizations when the time is right.