Skip to content
Volume E · The TensorCore.si Library

Elixir

Fault-tolerant concurrency on the BEAM.

Elixir is a functional language that runs on the Erlang virtual machine (BEAM), the system that powers telecom switches and WhatsApp. It makes it easy to build apps that handle millions of connections and recover from crashes, and its Nx and Bumblebee libraries bring machine learning to the BEAM.

Prologue

Why learn Elixir?

When an AI product needs thousands of live users, streaming responses and no downtime, Elixir is a strong choice.

  • Concurrency: lightweight processes let you run millions of tasks at once.
  • Fault tolerance: supervisors restart crashed parts automatically.
  • ML on the BEAM: Nx, Axon and Bumblebee run models and serve them in production.
Prologue

Set up and run your first program

Install Elixir (it brings Erlang). Use iex for an interactive shell and mix for projects.

terminal
elixir --version
elixir hello.exs
mix new my_app
hello.exs
name = "Ada"
IO.puts("Hello, AI! Welcome to Elixir, #{name}.")
Tip.exs files are scripts; .ex files are compiled as part of a project.
Chapter 1

Values and pattern matching

Data is immutable. = is a match, not just assignment: it can pull values out of tuples, lists and maps.

basics.exs
layers = 32
model = "llama3.1"
{:ok, gb} = {:ok, 80}            # pattern match on a tuple
[first | rest] = ["H100", "H200", "B200"]
IO.puts("#{model}: #{layers} layers")
IO.inspect({gb, first, rest})
IO.puts(8.03e9 * 2 / 1.0e9)
Chapter 2

Modules and functions

Functions live in modules. Multiple clauses with guards replace if-chains. The pipe |> passes a value into the next function.

functions.exs
defmodule Precision do
  def name(32), do: "FP32"
  def name(16), do: "FP16"
  def name(bits) when bits < 16, do: "low precision"

  def params(layers, width \\ 4096), do: 12 * layers * width * width
end

IO.puts(Precision.name(8))
"  Hello Elixir " |> String.trim() |> String.upcase() |> IO.puts()
Chapter 3

Lists, maps and Enum

The Enum module covers mapping, filtering and reducing. Maps hold key-value data.

collections.exs
vram = %{"H100" => 80, "H200" => 141, "RTX 5090" => 32}
big = for {name, gb} <- vram, gb >= 80, do: name
total = vram |> Map.values() |> Enum.sum()
squares = Enum.map(1..5, fn x -> x * x end)
IO.inspect(big)
IO.inspect({total, squares})
Chapter 4

Processes and messages

spawn starts a lightweight process. Processes share nothing and talk by sending messages. Task makes parallel work easy.

processes.exs
parent = self()
spawn(fn -> send(parent, {:done, "H100 benchmark"}) end)
receive do
  {:done, job} -> IO.puts("finished " <> job)
end

results =
  ["llama", "qwen", "mistral"]
  |> Task.async_stream(fn m -> {m, String.length(m)} end)
  |> Enum.map(fn {:ok, r} -> r end)
IO.inspect(results)
Chapter 5

GenServer and supervision

OTP behaviours like GenServer hold state in a process, and supervisors restart them if they crash.

otp.exs
defmodule Counter do
  use GenServer
  def start_link(n), do: GenServer.start_link(__MODULE__, n, name: __MODULE__)
  def add(x), do: GenServer.cast(__MODULE__, {:add, x})
  def value, do: GenServer.call(__MODULE__, :value)
  def init(n), do: {:ok, n}
  def handle_cast({:add, x}, n), do: {:noreply, n + x}
  def handle_call(:value, _from, n), do: {:reply, n, n}
end

Supervisor.start_link([{Counter, 0}], strategy: :one_for_one)
Counter.add(5)
IO.puts(Counter.value())
Part II · AI with Elixir

AI with Elixir

The Nx ecosystem brings numerical computing to Elixir: Nx tensors compile to CPU or GPU through EXLA (XLA), Axon builds neural networks, and Bumblebee runs Hugging Face models with a few lines of code. Phoenix LiveView is popular for streaming chat interfaces.

The essential libraries

NxTensors and numerical definitions, GPU via EXLA.
AxonNeural networks built on Nx.
BumblebeePretrained Hugging Face models.
ReqSimple HTTP client for LLM APIs.

Run a Hugging Face model

Bumblebee downloads a sentiment model and serves it with Nx.Serving.

sentiment.exs
Mix.install([{:bumblebee, "~> 0.6"}, {:exla, ">= 0.0.0"}])
Nx.global_default_backend(EXLA.Backend)

repo = {:hf, "distilbert/distilbert-base-uncased-finetuned-sst-2-english"}
{:ok, model} = Bumblebee.load_model(repo)
{:ok, tokenizer} = Bumblebee.load_tokenizer(repo)
serving = Bumblebee.Text.text_classification(model, tokenizer)

Nx.Serving.run(serving, "Tensor cores make training so much faster!")
|> IO.inspect()
Part III · Build an AI agent

Build an AI agent in Elixir

With the Req HTTP client, an agent is a recursive function that keeps calling the model until it stops asking for tools.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
agent.exs
Mix.install([{:req, "~> 0.5"}])

defmodule ChatAgent do
  @url "http://localhost:11434/v1/chat/completions"
  @tools [%{type: "function", function: %{name: "get_weather", description: "Weather for a city",
    parameters: %{type: "object", properties: %{city: %{type: "string"}}, required: ["city"]}}}]

  def get_weather(%{"city" => city}), do: "It is 24°C and sunny in #{city}."

  def run(messages, rounds \\ 5)
  def run(_messages, 0), do: "gave up"
  def run(messages, rounds) do
    body = %{model: "llama3.1", messages: messages, tools: @tools}
    msg = Req.post!(@url, json: body).body["choices"] |> hd() |> Map.get("message")
    case msg["tool_calls"] do
      calls when is_list(calls) and calls != [] ->
        replies = for c <- calls do
          args = Jason.decode!(c["function"]["arguments"])
          %{role: "tool", tool_call_id: c["id"], content: get_weather(args)}
        end
        run(messages ++ [msg | replies], rounds - 1)
      _ -> msg["content"]
    end
  end
end

IO.puts(ChatAgent.run([%{role: "user", content: "Should I take an umbrella in Lahore today?"}]))

In a Phoenix app, run each conversation in its own process so one slow request never blocks the rest.

Run it locallyInstall Ollama, run ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.
Part IV · Your path

Elixir learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

Word frequencyCount words in a file with Enum and Map.
Chat roomReal-time chat with Phoenix channels.
AI chat appLiveView chat that streams answers from an LLM.
Reference

Elixir cheat sheet

TaskSyntax
Interpolation"Hi #{name}"
Anonymous fnfn x -> x * 2 end
Pipex |> f() |> g()
Map%{"a" => 1}
Comprehensionfor x <- 1..5, do: x * x
Spawnspawn(fn -> ... end)
Reference

Common mistakes

  • Data is immutable: functions return new values, they never change the old one.
  • Calling an anonymous function needs a dot: f.(x).
  • Use <> to join strings, not +.
Reference

Where to go next

On TensorCore.si: Erlang volume, Python volume.

Keyboard: [ previous language · ] next language · / search the library