Current section

Files

Jump to
llama_cpp_ex lib llama_cpp_ex.ex
Raw

lib/llama_cpp_ex.ex

defmodule LlamaCppEx do
@moduledoc """
Elixir bindings for llama.cpp.
Provides a high-level API for loading GGUF models and generating text.
## Quick Start
# Initialize the backend (once per application)
:ok = LlamaCppEx.init()
# Load a model
{:ok, model} = LlamaCppEx.load_model("model.gguf", n_gpu_layers: -1)
# Generate text
{:ok, text} = LlamaCppEx.generate(model, "Once upon a time", max_tokens: 200)
## Lower-level API
For fine-grained control, use the individual modules:
* `LlamaCppEx.Model` - Model loading and introspection
* `LlamaCppEx.Context` - Inference context with KV cache
* `LlamaCppEx.Sampler` - Token sampling configuration
* `LlamaCppEx.Tokenizer` - Text tokenization and detokenization
* `LlamaCppEx.Embedding` - Embedding generation
"""
alias LlamaCppEx.{Model, Context, Sampler, Tokenizer, Chat, Embedding}
@doc """
Initializes the llama.cpp backend. Call once at application start.
"""
@spec init() :: :ok
def init do
LlamaCppEx.NIF.backend_init()
end
@doc """
Loads a GGUF model from the given file path.
See `LlamaCppEx.Model.load/2` for options.
"""
@spec load_model(String.t(), keyword()) :: {:ok, Model.t()} | {:error, String.t()}
def load_model(path, opts \\ []) do
Model.load(path, opts)
end
@doc """
Generates text from a prompt.
Creates a temporary context and sampler, tokenizes the prompt, runs generation,
and returns the generated text.
## Options
* `:max_tokens` - Maximum tokens to generate. Defaults to `256`.
* `:n_ctx` - Context size. Defaults to `2048`.
* `:temp` - Sampling temperature. `0.0` for greedy. Defaults to `0.8`.
* `:top_k` - Top-K filtering. Defaults to `40`.
* `:top_p` - Top-P (nucleus) filtering. Defaults to `0.95`.
* `:min_p` - Min-P filtering. Defaults to `0.05`.
* `:seed` - Random seed. Defaults to random.
* `:penalty_repeat` - Repetition penalty. Defaults to `1.0`.
* `:grammar` - GBNF grammar string for constrained generation.
* `:grammar_root` - Root rule name for grammar. Defaults to `"root"`.
"""
@spec generate(Model.t(), String.t(), keyword()) :: {:ok, String.t()} | {:error, String.t()}
def generate(%Model{} = model, prompt, opts \\ []) when is_binary(prompt) do
max_tokens = Keyword.get(opts, :max_tokens, 256)
n_ctx = Keyword.get(opts, :n_ctx, 2048)
sampler_opts =
Keyword.take(opts, [
:seed,
:temp,
:top_k,
:top_p,
:min_p,
:penalty_repeat,
:grammar,
:grammar_root
])
# Tokenize prompt
{:ok, tokens} = Tokenizer.encode(model, prompt)
# Ensure context is large enough for prompt + generation
ctx_size = max(n_ctx, length(tokens) + max_tokens)
ctx_opts =
opts
|> Keyword.take([:n_threads, :n_threads_batch, :n_batch, :n_ubatch])
|> Keyword.put(:n_ctx, ctx_size)
with {:ok, ctx} <- Context.create(model, ctx_opts),
{:ok, sampler} <- Sampler.create(model, sampler_opts) do
Context.generate(ctx, sampler, tokens, max_tokens: max_tokens)
end
end
@doc """
Returns a lazy stream of generated text chunks (tokens).
Each element is a string (the text piece for one token). The stream ends
when an end-of-generation token is produced or `max_tokens` is reached.
Accepts the same options as `generate/3`.
## Examples
model
|> LlamaCppEx.stream("Tell me a story", max_tokens: 500)
|> Enum.each(&IO.write/1)
"""
@spec stream(Model.t(), String.t(), keyword()) :: Enumerable.t()
def stream(%Model{} = model, prompt, opts \\ []) when is_binary(prompt) do
max_tokens = Keyword.get(opts, :max_tokens, 256)
n_ctx = Keyword.get(opts, :n_ctx, 2048)
timeout = Keyword.get(opts, :timeout, 60_000)
sampler_opts =
Keyword.take(opts, [
:seed,
:temp,
:top_k,
:top_p,
:min_p,
:penalty_repeat,
:grammar,
:grammar_root
])
ctx_opts =
Keyword.take(opts, [:n_threads, :n_threads_batch, :n_batch, :n_ubatch])
Stream.resource(
fn ->
# Start: tokenize, create context+sampler, spawn generator
{:ok, tokens} = Tokenizer.encode(model, prompt)
ctx_size = max(n_ctx, length(tokens) + max_tokens)
{:ok, ctx} = Context.create(model, Keyword.put(ctx_opts, :n_ctx, ctx_size))
{:ok, sampler} = Sampler.create(model, sampler_opts)
ref = make_ref()
parent = self()
gen_pid =
spawn_link(fn ->
LlamaCppEx.NIF.generate_tokens(
ctx.ref,
sampler.ref,
tokens,
max_tokens,
parent,
ref
)
end)
{ref, gen_pid, timeout}
end,
fn {ref, _gen_pid, timeout} = state ->
receive do
{^ref, {:token, _id, text}} -> {[text], state}
{^ref, :eog} -> {:halt, state}
{^ref, :done} -> {:halt, state}
{^ref, {:error, _reason}} -> {:halt, state}
after
timeout -> {:halt, state}
end
end,
fn {ref, gen_pid, _timeout} ->
# Kill generator if still running, flush remaining messages
Process.unlink(gen_pid)
Process.exit(gen_pid, :kill)
flush_stream_messages(ref)
end
)
end
defp flush_stream_messages(ref) do
receive do
{^ref, _} -> flush_stream_messages(ref)
after
0 -> :ok
end
end
@doc """
Applies the chat template and generates a response.
## Options
Accepts all options from `generate/3` plus:
* `:template` - Custom chat template string. Defaults to the model's embedded template.
## Examples
{:ok, reply} = LlamaCppEx.chat(model, [
%{role: "system", content: "You are helpful."},
%{role: "user", content: "What is Elixir?"}
], max_tokens: 200)
"""
@spec chat(Model.t(), [Chat.message()], keyword()) :: {:ok, String.t()} | {:error, String.t()}
def chat(%Model{} = model, messages, opts \\ []) when is_list(messages) do
{chat_opts, gen_opts} = Keyword.split(opts, [:template, :add_assistant])
{:ok, prompt} = Chat.apply_template(model, messages, chat_opts)
generate(model, prompt, gen_opts)
end
@doc """
Returns a lazy stream of chat response chunks.
Applies the chat template and streams the generated response token by token.
Accepts same options as `chat/3`.
"""
@spec stream_chat(Model.t(), [Chat.message()], keyword()) :: Enumerable.t()
def stream_chat(%Model{} = model, messages, opts \\ []) when is_list(messages) do
{chat_opts, gen_opts} = Keyword.split(opts, [:template, :add_assistant])
{:ok, prompt} = Chat.apply_template(model, messages, chat_opts)
stream(model, prompt, gen_opts)
end
@doc """
Computes an embedding for a single text.
See `LlamaCppEx.Embedding.embed/3` for options.
"""
@spec embed(Model.t(), String.t(), keyword()) :: {:ok, Embedding.t()} | {:error, String.t()}
def embed(%Model{} = model, text, opts \\ []) do
Embedding.embed(model, text, opts)
end
@doc """
Computes embeddings for multiple texts.
See `LlamaCppEx.Embedding.embed_batch/3` for options.
"""
@spec embed_batch(Model.t(), [String.t()], keyword()) ::
{:ok, [Embedding.t()]} | {:error, String.t()}
def embed_batch(%Model{} = model, texts, opts \\ []) do
Embedding.embed_batch(model, texts, opts)
end
end