Packages
llama_cpp_ex
0.8.42
0.8.42
0.8.41
0.8.39
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.28
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.0
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.3.0
0.2.0
Elixir bindings for llama.cpp — run LLMs locally with Metal, CUDA, Vulkan, or CPU acceleration.
Current section
Files
Jump to
Current section
Files
llama_cpp_ex
mix.exs
mix.exs
defmodule LlamaCppEx.Precompiler do
@moduledoc false
# Linux CUDA artifacts are published per CUDA major version because the NIF
# links libcudart, libcublas and libcublasLt dynamically and their sonames are
# major-versioned: libcudart.so.12 against a CUDA 13 install does not resolve,
# and there is no compatibility shim in either direction. So the variant has
# to be part of the target name -- one "linux CUDA" artifact cannot exist.
#
# Newest first: a host with both toolkits installed should get the newer one.
@cuda_majors ["13", "12"]
# Only x86_64 Linux gets CUDA variants today. aarch64 Linux (DGX Spark and
# friends) still resolves to no artifact and source-builds, which the
# Makefile's toolkit discovery now handles; adding it here is a matrix entry
# in .github/workflows/precompile.yml plus this list.
@cuda_targets for major <- @cuda_majors, do: "x86_64-linux-gnu-cu#{major}"
@all_targets ["aarch64-apple-darwin", "x86_64-linux-gnu"] ++ @cuda_targets
# Set by each CUDA leg of the precompile workflow. Detection below deliberately
# refuses to name a CUDA target on a machine with no driver, which is exactly
# what a release runner is, so the build has to state its own variant.
# Also the escape hatch for a host whose layout defeats the probe: "cu12",
# "cu13", or "none" to force the CPU artifact.
@variant_env "LLAMA_CUDA_VARIANT"
# Where a CUDA runtime shows up when ldconfig has nothing to say -- a container
# with no ldconfig cache, or an install that was never registered.
@cuda_lib_globs [
"/usr/local/cuda/lib64",
"/usr/local/cuda-*/lib64",
"/usr/local/cuda-*/targets/*/lib",
"/usr/lib/x86_64-linux-gnu",
"/usr/lib64"
]
def all_supported_targets(:fetch), do: @all_targets
def all_supported_targets(:compile) do
case current_target() do
{:ok, target} -> [target]
_ -> []
end
end
def current_target do
system_arch = to_string(:erlang.system_info(:system_architecture))
cond do
system_arch =~ ~r/aarch64.*apple.*darwin/ -> {:ok, "aarch64-apple-darwin"}
system_arch =~ ~r/x86_64.*linux.*gnu/ -> {:ok, "x86_64-linux-gnu" <> cuda_suffix()}
true -> {:error, "unsupported target: #{system_arch}"}
end
end
@doc false
# Exposed for tests: the probe is pure over the two facts it looks up, so the
# interesting cases can be exercised without a CUDA install.
def cuda_suffix(env \\ &System.get_env/1, present? \\ &library_present?/1) do
case env.(@variant_env) do
nil -> detect_cuda_suffix(present?)
"" -> ""
"none" -> ""
"cu" <> major when major in @cuda_majors -> "-cu#{major}"
other -> raise ArgumentError, bad_variant_message(other)
end
end
defp bad_variant_message(value) do
allowed = Enum.map_join(@cuda_majors, ", ", &"cu#{&1}")
"#{@variant_env}=#{inspect(value)} is not a known CUDA variant (#{allowed}, none)"
end
# Two conditions, both required. The runtime libraries are what the artifact
# links against, and the driver is what -lcuda resolves to at load time: a
# machine with the toolkit but no driver cannot dlopen a CUDA build at all, so
# handing it one would turn a working CPU install into a NIF that fails to
# load. nvcc is deliberately not consulted -- running a CUDA build needs no
# compiler, and plenty of GPU hosts have no toolkit installed.
defp detect_cuda_suffix(present?) do
if present?.("libcuda.so.1") do
Enum.find_value(@cuda_majors, "", fn major ->
if present?.("libcudart.so.#{major}"), do: "-cu#{major}"
end)
else
""
end
end
defp library_present?(soname) do
ldconfig_lists?(soname) or on_disk?(soname)
end
defp ldconfig_lists?(soname) do
# ldconfig lives in /sbin, which is routinely off a non-root PATH.
case Enum.find(
["ldconfig", "/sbin/ldconfig", "/usr/sbin/ldconfig"],
&System.find_executable/1
) do
nil ->
false
ldconfig ->
case System.cmd(ldconfig, ["-p"], stderr_to_stdout: true) do
{output, 0} -> String.contains?(output, soname)
_ -> false
end
end
catch
# An ldconfig that is present but unusable is a "no", never a build failure.
_, _ -> false
end
defp on_disk?(soname) do
Enum.any?(@cuda_lib_globs, fn glob ->
glob |> Path.join(soname) |> Path.wildcard() |> Enum.any?()
end)
end
def build_native(args), do: ElixirMake.Precompiler.mix_compile(args)
def precompile(args, _target) do
case ElixirMake.Precompiler.mix_compile(args) do
{:ok, _} -> :ok
error -> error
end
end
def unavailable_target(_target), do: :compile
end
defmodule LlamaCppEx.MixProject do
use Mix.Project
@version "0.8.42"
@source_url "https://github.com/nyo16/llama_cpp_ex"
def project do
[
app: :llama_cpp_ex,
version: @version,
elixir: "~> 1.18",
start_permanent: Mix.env() == :prod,
deps: deps(),
compilers: Mix.compilers() ++ [:elixir_make],
make_env: &make_env/0,
make_clean: ["clean"],
make_precompiler: {:nif, LlamaCppEx.Precompiler},
make_precompiler_url:
"https://github.com/nyo16/llama_cpp_ex/releases/download/v#{@version}/@{artefact_filename}",
make_precompiler_filename: "llama_cpp_ex_nif",
make_precompiler_priv_paths: ["llama_cpp_ex_nif.so"],
# Verified against erts/emulator/beam/erl_nif.h in the OTP source: OTP 25
# reports NIF 2.16, OTP 26/27/28 report 2.17, and OTP 29 reports 2.18.
# Only 2.17 and 2.18 artifacts are built, so the precompiled floor is
# OTP 26. Declaring a "2.16" entry would be worse than omitting it: no
# such artifact exists, so `mix elixir_make.checksum --all` could not
# vouch for it. On OTP 25 elixir_make finds no matching artifact and
# recovers with a source build (see compile.elixir_make.ex), which the
# Makefile's llama.cpp clone fallback now makes possible.
make_precompiler_nif_versions: [versions: ["2.17", "2.18"]],
make_force_build: System.get_env("LLAMA_BACKEND") != nil,
description: description(),
package: package(),
name: "LlamaCppEx",
source_url: @source_url,
homepage_url: @source_url,
docs: docs(),
# Keep the PLT where CI caches it (priv/plts). Without this dialyxir
# writes under _build and the cache never hits.
dialyzer: [
plt_local_path: "priv/plts",
plt_core_path: "priv/plts"
],
test_coverage: [summary: [threshold: 0]],
# test/support/test_models.exs is a helper module that test_helper.exs
# loads with Code.require_file/2, not a test file. Elixir 1.20 warns about
# unmatched files under test/ unless they are filtered out here.
test_ignore_filters: [~r{^test/support/}]
]
end
def application do
[extra_applications: [:logger], mod: {LlamaCppEx.Application, []}]
end
defp deps do
[
{:elixir_make, "~> 0.8", runtime: false},
{:fine, "~> 0.1", runtime: false},
{:telemetry, "~> 1.0"},
{:ecto, "~> 3.0", optional: true},
{:req, "~> 0.5 or ~> 0.6", optional: true},
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
{:benchee, "~> 1.0", only: :bench, runtime: false},
{:benchee_html, "~> 1.0", only: :bench, runtime: false}
]
end
defp description do
"Elixir bindings for llama.cpp — run LLMs locally with Metal, CUDA, Vulkan, or CPU acceleration."
end
defp package do
[
name: "llama_cpp_ex",
licenses: ["Apache-2.0"],
links: %{
"GitHub" => @source_url,
"Changelog" => "https://hexdocs.pm/llama_cpp_ex/changelog.html",
"llama.cpp" => "https://github.com/ggml-org/llama.cpp"
},
# vendor/llama.cpp is deliberately not shipped: it would add hundreds of
# megabytes to every release. .gitmodules is shipped instead so the
# Makefile can read the upstream URL and clone the pinned commit when a
# source build needs it.
files: ~w(
lib c_src Makefile mix.exs README.md CHANGELOG.md LICENSE .formatter.exs
checksum.exs .gitmodules
)
]
end
defp docs do
[
main: "readme",
extras: [
"README.md",
"CHANGELOG.md",
"LICENSE",
"docs/architecture.md",
"docs/cross-platform-builds.md",
"docs/adr/001-cpp-nif-over-rustler.md",
"docs/adr/002-fine-for-nif-ergonomics.md",
"docs/adr/003-static-linking.md",
"docs/adr/004-streaming-via-enif-send.md",
"docs/adr/005-batching-architecture.md",
"docs/adr/006-continuous-batching.md",
"docs/adr/007-prefix-caching.md",
"docs/adr/008-batching-strategies.md",
"docs/examples.md",
"docs/performance.md",
"docs/release-guide.md"
],
groups_for_extras: [
"Architecture Decision Records": ~r/docs\/adr\/.*/
],
groups_for_modules: [
"High-Level API": [LlamaCppEx],
"Core Modules": [
LlamaCppEx.Model,
LlamaCppEx.Context,
LlamaCppEx.Sampler,
LlamaCppEx.Tokenizer,
LlamaCppEx.Chat,
LlamaCppEx.Embedding,
LlamaCppEx.Grammar,
LlamaCppEx.Schema,
LlamaCppEx.Server,
LlamaCppEx.Hub
],
"Batching Strategies": [
LlamaCppEx.Server.BatchStrategy,
LlamaCppEx.Server.Strategy.DecodeMaximal,
LlamaCppEx.Server.Strategy.PrefillPriority,
LlamaCppEx.Server.Strategy.Balanced
],
Internal: [LlamaCppEx.NIF]
]
]
end
# Environment variables forwarded to the Makefile. System.cmd merges :env into
# the inherited environment anyway, but listing them keeps the build's input
# contract in one place:
#
# LLAMA_BACKEND auto | metal | cuda | vulkan | cpu
# LLAMA_CMAKE_ARGS extra flags appended to the llama.cpp cmake invocation
# LLAMA_PORTABLE 1 to drop -march=native, set by the precompile workflow
# LLAMA_CUDA_NCCL 1 to build and link ggml's NCCL multi-GPU collectives,
# which also makes libnccl.so.2 a load-time requirement
@make_env_passthrough [
"LLAMA_BACKEND",
"LLAMA_CMAKE_ARGS",
"LLAMA_PORTABLE",
"LLAMA_CUDA_NCCL"
]
defp make_env do
base = %{"FINE_INCLUDE_DIR" => Fine.include_dir()}
Enum.reduce(@make_env_passthrough, base, fn key, env ->
case System.get_env(key) do
nil -> env
value -> Map.put(env, key, value)
end
end)
end
end