Packages

vLLM for Elixir via SnakeBridge - Easy, fast, and cheap LLM serving for everyone. High-throughput LLM inference with PagedAttention, continuous batching, and OpenAI-compatible API.

Current section

Files

Jump to
vllm lib vllm.ex
Raw

lib/vllm.ex

defmodule VLLM do
@moduledoc """
VLLM - vLLM for Elixir via SnakeBridge.
Easy, fast, and cheap LLM serving for everyone. This library provides
transparent access to Python vLLM through SnakeBridge's Universal FFI.
## Quick Start
VLLM.run(fn ->
# Create an LLM instance
llm = VLLM.llm!("facebook/opt-125m")
# Generate text
outputs = VLLM.generate!(llm, ["Hello, my name is"])
# Process results
Enum.each(outputs, fn output ->
prompt = VLLM.attr!(output, "prompt")
generated = VLLM.attr!(output, "outputs") |> Enum.at(0)
text = VLLM.attr!(generated, "text")
IO.puts("Prompt: \#{prompt}")
IO.puts("Generated: \#{text}")
end)
end)
## Chat Interface
VLLM.run(fn ->
llm = VLLM.llm!("Qwen/Qwen2-0.5B-Instruct")
messages = [[
%{"role" => "system", "content" => "You are a helpful assistant."},
%{"role" => "user", "content" => "What is the capital of France?"}
]]
outputs = VLLM.chat!(llm, messages)
# Process chat outputs...
end)
## Sampling Parameters
Control generation with `VLLM.SamplingParams`:
VLLM.run(fn ->
llm = VLLM.llm!("facebook/opt-125m")
params = VLLM.sampling_params!(temperature: 0.8, top_p: 0.95, max_tokens: 100)
outputs = VLLM.generate!(llm, ["Once upon a time"], sampling_params: params)
end)
## Timeout Configuration
VLLM leverages SnakeBridge's timeout architecture for LLM workloads.
By default, all vLLM calls use the `:ml_inference` profile (10 minute timeout).
### Timeout Profiles
| Profile | Timeout | Use Case |
|-----------------|----------|---------------------------------------|
| `:default` | 2 min | Standard Python calls |
| `:streaming` | 30 min | Streaming responses |
| `:ml_inference` | 10 min | LLM inference (VLLM default) |
| `:batch_job` | 1 hour | Long-running batch operations |
### Per-Call Timeout Override
VLLM.generate!(llm, prompts,
sampling_params: params,
__runtime__: [timeout_profile: :batch_job]
)
## Architecture
VLLM uses SnakeBridge's Universal FFI to call vLLM directly:
Elixir (VLLM.call/4)
|
SnakeBridge.call/4
|
Snakepit gRPC
|
Python vLLM
|
GPU/TPU Inference
All Python lifecycle is managed automatically by Snakepit.
"""
# ---------------------------------------------------------------------------
# Lifecycle Management
# ---------------------------------------------------------------------------
@doc """
Run VLLM code with automatic Python lifecycle management.
Wraps your code in `Snakepit.run_as_script/2` which:
- Starts the Python process pool
- Runs your code
- Cleans up on exit
Pass `halt: true` in opts if you need to force the BEAM to exit
(for example, when running inside wrapper scripts).
## Example
VLLM.run(fn ->
llm = VLLM.llm!("facebook/opt-125m")
outputs = VLLM.generate!(llm, ["Hello, world"])
# ... process outputs
end)
"""
def run(fun, opts \\ []) when is_function(fun, 0) do
Snakepit.run_as_script(fun, opts)
end
# ---------------------------------------------------------------------------
# Core LLM API
# ---------------------------------------------------------------------------
@doc """
Create a vLLM LLM instance for offline inference.
## Options
Common options passed as keyword arguments:
* `:dtype` - Data type ("auto", "float16", "bfloat16", "float32")
* `:tensor_parallel_size` - Number of GPUs for tensor parallelism
* `:gpu_memory_utilization` - Fraction of GPU memory to use (0.0-1.0)
* `:max_model_len` - Maximum sequence length
* `:quantization` - Quantization method ("awq", "gptq", "squeezellm", etc.)
* `:trust_remote_code` - Whether to trust remote code from HuggingFace
## Examples
{:ok, llm} = VLLM.llm("facebook/opt-125m")
{:ok, llm} = VLLM.llm("Qwen/Qwen2-7B", tensor_parallel_size: 2)
{:ok, llm} = VLLM.llm("TheBloke/Llama-2-7B-AWQ", quantization: "awq")
"""
def llm(model, opts \\ []) do
SnakeBridge.call("vllm", "LLM", [model], opts)
end
@doc "Bang version of llm/2 - raises on error."
def llm!(model, opts \\ []) do
SnakeBridge.call!("vllm", "LLM", [model], opts)
end
@doc """
Create SamplingParams for controlling text generation.
## Options
* `:temperature` - Sampling temperature (default: 1.0)
* `:top_p` - Nucleus sampling probability (default: 1.0)
* `:top_k` - Top-k sampling (default: -1, disabled)
* `:max_tokens` - Maximum tokens to generate (default: 16)
* `:min_tokens` - Minimum tokens to generate (default: 0)
* `:presence_penalty` - Presence penalty (default: 0.0)
* `:frequency_penalty` - Frequency penalty (default: 0.0)
* `:repetition_penalty` - Repetition penalty (default: 1.0)
* `:stop` - List of stop strings
* `:stop_token_ids` - List of stop token IDs
* `:n` - Number of completions to generate (default: 1)
* `:best_of` - Number of sequences to generate and select best from
* `:seed` - Random seed for reproducibility
## Examples
{:ok, params} = VLLM.sampling_params(temperature: 0.8, max_tokens: 100)
{:ok, params} = VLLM.sampling_params(top_p: 0.9, stop: ["\\n", "END"])
"""
def sampling_params(opts \\ []) do
SnakeBridge.call("vllm", "SamplingParams", [], opts)
end
@doc "Bang version of sampling_params/1 - raises on error."
def sampling_params!(opts \\ []) do
SnakeBridge.call!("vllm", "SamplingParams", [], opts)
end
@doc """
Generate text completions from prompts.
## Arguments
* `llm` - LLM instance from `VLLM.llm!/1`
* `prompts` - String or list of strings to complete
* `opts` - Options including:
* `:sampling_params` - SamplingParams instance
* `:use_tqdm` - Show progress bar (default: true)
* `:lora_request` - LoRA adapter request
## Examples
outputs = VLLM.generate!(llm, "Hello, my name is")
outputs = VLLM.generate!(llm, ["Prompt 1", "Prompt 2"], sampling_params: params)
## Returns
List of RequestOutput objects. Each has:
* `prompt` - Original prompt
* `outputs` - List of CompletionOutput objects
* `text` - Generated text
* `token_ids` - Generated token IDs
* `finish_reason` - Reason for completion ("length", "stop", etc.)
"""
def generate(llm, prompts, opts \\ []) do
prompts = List.wrap(prompts)
SnakeBridge.method(llm, "generate", [prompts], opts)
end
@doc "Bang version of generate/3 - raises on error."
def generate!(llm, prompts, opts \\ []) do
prompts = List.wrap(prompts)
SnakeBridge.method!(llm, "generate", [prompts], opts)
end
@doc """
Generate chat completions from messages.
## Arguments
* `llm` - LLM instance from `VLLM.llm!/1`
* `messages` - List of message conversations, where each conversation is a list of message maps
* `opts` - Options including:
* `:sampling_params` - SamplingParams instance
* `:use_tqdm` - Show progress bar
* `:chat_template` - Custom chat template (Jinja2 format)
## Message Format
Each message is a map with:
* `"role"` - One of "system", "user", "assistant"
* `"content"` - Message content string
## Examples
messages = [[
%{"role" => "system", "content" => "You are helpful."},
%{"role" => "user", "content" => "Hello!"}
]]
outputs = VLLM.chat!(llm, messages)
## Returns
List of RequestOutput objects (same as generate/3).
"""
def chat(llm, messages, opts \\ []) do
SnakeBridge.method(llm, "chat", [messages], opts)
end
@doc "Bang version of chat/3 - raises on error."
def chat!(llm, messages, opts \\ []) do
SnakeBridge.method!(llm, "chat", [messages], opts)
end
@doc """
Encode text to token IDs.
## Examples
{:ok, token_ids} = VLLM.encode(llm, "Hello, world!")
"""
def encode(llm, text, opts \\ []) do
SnakeBridge.method(llm, "encode", [text], opts)
end
@doc "Bang version of encode/3."
def encode!(llm, text, opts \\ []) do
SnakeBridge.method!(llm, "encode", [text], opts)
end
# ---------------------------------------------------------------------------
# Engine API (for advanced usage)
# ---------------------------------------------------------------------------
@doc """
Create an LLMEngine for fine-grained control over inference.
The LLMEngine provides lower-level access to vLLM's inference capabilities,
useful for building custom serving solutions.
## Options
Same as `llm/2` plus:
* `:max_num_seqs` - Maximum number of sequences per batch
* `:max_num_batched_tokens` - Maximum tokens per batch
## Examples
{:ok, engine} = VLLM.engine("facebook/opt-125m")
"""
def engine(model, opts \\ []) do
SnakeBridge.call("vllm", "LLMEngine", [model], opts)
end
@doc "Bang version of engine/2."
def engine!(model, opts \\ []) do
SnakeBridge.call!("vllm", "LLMEngine", [model], opts)
end
@doc """
Create an AsyncLLMEngine for asynchronous inference.
Useful for building online serving applications with concurrent requests.
## Examples
{:ok, engine} = VLLM.async_engine("facebook/opt-125m")
"""
def async_engine(model, opts \\ []) do
SnakeBridge.call("vllm.engine.async_llm_engine", "AsyncLLMEngine", [model], opts)
end
@doc "Bang version of async_engine/2."
def async_engine!(model, opts \\ []) do
SnakeBridge.call!("vllm.engine.async_llm_engine", "AsyncLLMEngine", [model], opts)
end
# ---------------------------------------------------------------------------
# Pooling (Embeddings) API
# ---------------------------------------------------------------------------
@doc """
Create PoolingParams for embedding models.
## Options
* `:additional_data` - Additional metadata for the pooling request
## Examples
{:ok, params} = VLLM.pooling_params()
"""
def pooling_params(opts \\ []) do
SnakeBridge.call("vllm", "PoolingParams", [], opts)
end
@doc "Bang version of pooling_params/1."
def pooling_params!(opts \\ []) do
SnakeBridge.call!("vllm", "PoolingParams", [], opts)
end
@doc """
Generate embeddings for texts using a pooling model.
## Arguments
* `llm` - LLM instance configured with an embedding model
* `texts` - String or list of strings to embed
* `opts` - Options including:
* `:pooling_params` - PoolingParams instance
## Examples
llm = VLLM.llm!("intfloat/e5-mistral-7b-instruct", runner: "pooling")
outputs = VLLM.embed!(llm, ["Hello, world!", "How are you?"])
## Returns
List of EmbeddingRequestOutput objects with:
* `outputs` - List of embeddings
"""
def embed(llm, texts, opts \\ []) do
texts = List.wrap(texts)
SnakeBridge.method(llm, "embed", [texts], opts)
end
@doc "Bang version of embed/3."
def embed!(llm, texts, opts \\ []) do
texts = List.wrap(texts)
SnakeBridge.method!(llm, "embed", [texts], opts)
end
# ---------------------------------------------------------------------------
# LoRA Support
# ---------------------------------------------------------------------------
@doc """
Create a LoRARequest for serving LoRA adapters.
## Arguments
* `name` - Unique name for this LoRA adapter
* `lora_int_id` - Integer ID for the adapter
* `lora_path` - Path to the LoRA adapter weights
## Examples
{:ok, lora} = VLLM.lora_request("my-adapter", 1, "/path/to/adapter")
"""
def lora_request(name, lora_int_id, lora_path, opts \\ []) do
SnakeBridge.call("vllm.lora.request", "LoRARequest", [name, lora_int_id, lora_path], opts)
end
@doc "Bang version of lora_request/4."
def lora_request!(name, lora_int_id, lora_path, opts \\ []) do
SnakeBridge.call!("vllm.lora.request", "LoRARequest", [name, lora_int_id, lora_path], opts)
end
# ---------------------------------------------------------------------------
# Guided Generation / Structured Outputs
# ---------------------------------------------------------------------------
@doc """
Create guided decoding parameters for structured outputs.
## Options
* `:json` - JSON schema string for JSON output
* `:json_object` - Python dict/Pydantic model for JSON
* `:regex` - Regex pattern for output
* `:choice` - List of allowed string choices
* `:grammar` - BNF grammar string
## Examples
# JSON schema
{:ok, guided} = VLLM.guided_decoding_params(
json: ~s({"type": "object", "properties": {"name": {"type": "string"}}})
)
# Regex pattern
{:ok, guided} = VLLM.guided_decoding_params(regex: "[0-9]{3}-[0-9]{4}")
# Choice
{:ok, guided} = VLLM.guided_decoding_params(choice: ["yes", "no", "maybe"])
## Support
Guided decoding requires a vLLM build that exposes `GuidedDecodingParams`.
Use `guided_decoding_supported?/0` to check availability.
"""
def guided_decoding_params(opts \\ []) do
if guided_decoding_supported?() do
SnakeBridge.call("vllm", "GuidedDecodingParams", [], opts)
else
{:error, :guided_decoding_not_supported}
end
end
@doc "Bang version of guided_decoding_params/1."
def guided_decoding_params!(opts \\ []) do
case guided_decoding_params(opts) do
{:ok, guided} ->
guided
{:error, :guided_decoding_not_supported} ->
version =
case version() do
{:ok, value} -> value
{:error, _} -> "unknown"
end
raise ArgumentError,
"Guided decoding is not available in vLLM #{version}. " <>
"Upgrade vLLM or disable structured outputs."
{:error, error} ->
raise RuntimeError, message: "Guided decoding error: #{inspect(error)}"
end
end
@doc """
Check whether guided decoding parameters are available in the installed vLLM.
"""
def guided_decoding_supported? do
case SnakeBridge.get("vllm", "GuidedDecodingParams") do
{:ok, _} -> true
{:error, _} -> false
end
end
# ---------------------------------------------------------------------------
# Timeout helpers
# ---------------------------------------------------------------------------
@doc """
Add timeout configuration to options.
## Options
* `:timeout` - Exact timeout in milliseconds
* `:timeout_profile` - Use a predefined profile
## Examples
opts = VLLM.with_timeout([], timeout: 60_000)
VLLM.generate!(llm, prompts, Keyword.merge(opts, sampling_params: params))
"""
def with_timeout(opts, timeout_opts) when is_list(opts) and is_list(timeout_opts) do
runtime = Keyword.get(opts, :__runtime__, [])
new_runtime = Keyword.merge(runtime, timeout_opts)
Keyword.put(opts, :__runtime__, new_runtime)
end
@doc """
Timeout profile atoms for use with `__runtime__` option.
## Examples
VLLM.generate!(llm, prompts,
Keyword.merge([sampling_params: params], VLLM.timeout_profile(:batch_job))
)
"""
def timeout_profile(profile)
when profile in [:default, :streaming, :ml_inference, :batch_job] do
[__runtime__: [timeout_profile: profile]]
end
@doc """
Create a timeout option for exact milliseconds.
## Examples
VLLM.generate!(llm, prompts,
Keyword.merge([sampling_params: params], VLLM.timeout_ms(300_000))
)
"""
def timeout_ms(milliseconds) when is_integer(milliseconds) and milliseconds > 0 do
[__runtime__: [timeout: milliseconds]]
end
# ---------------------------------------------------------------------------
# Universal FFI pass-through (convenience re-exports)
# ---------------------------------------------------------------------------
@doc """
Call any vLLM function or class.
## Examples
{:ok, result} = VLLM.call("vllm", "LLM", ["facebook/opt-125m"])
{:ok, config} = VLLM.call("vllm.config", "ModelConfig", [], model: "...")
"""
defdelegate call(module, function, args \\ [], opts \\ []), to: SnakeBridge
@doc "Bang version - raises on error, returns value directly."
defdelegate call!(module, function, args \\ [], opts \\ []), to: SnakeBridge
@doc "Get a module attribute."
defdelegate get(module, attr), to: SnakeBridge
@doc "Bang version of get/2."
defdelegate get!(module, attr), to: SnakeBridge
@doc "Call a method on a Python object reference."
defdelegate method(ref, method, args \\ [], opts \\ []), to: SnakeBridge
@doc "Bang version of method/4."
defdelegate method!(ref, method, args \\ [], opts \\ []), to: SnakeBridge
@doc "Get an attribute from a Python object reference."
defdelegate attr(ref, attribute), to: SnakeBridge
@doc "Bang version of attr/2."
defdelegate attr!(ref, attribute), to: SnakeBridge
@doc "Set an attribute on a Python object reference."
defdelegate set_attr(ref, attribute, value), to: SnakeBridge
@doc "Check if a value is a Python object reference."
defdelegate ref?(value), to: SnakeBridge
@doc "Encode binary data as Python bytes."
defdelegate bytes(data), to: SnakeBridge
@doc "Get the installed vLLM version."
def version do
case SnakeBridge.get("vllm", "__version__") do
{:ok, value} -> {:ok, to_string(value)}
{:error, error} -> {:error, error}
end
end
@doc "Bang version of version/0."
def version! do
case version() do
{:ok, value} ->
value
{:error, error} ->
raise RuntimeError, message: "Unable to read vLLM version: #{inspect(error)}"
end
end
end