Open source library

Polar Llama

A Python library for parallel LLM inference across providers, built on Polars DataFrames.

v0.5.0Released July 4, 2026
View on GitHubPyPI package
Version0.5.2latest0.5.10.5.00.3.00.2.20.2.10.2.00.1.7
You are viewing documentation for version 0.5.0. A newer version (0.5.2) is available.

Overview

Polar Llama is a Python library that enables parallel inference calls to multiple Large Language Model providers through Polars dataframes. It streamlines batch processing of AI queries without serial request delays, making it ideal for data-intensive AI applications. 0.5.0 adds a local MLX inference backend for Apple Silicon, folding on-device generation into the same .llama expression API used for hosted providers — no keys, no network required.

Concurrent Processing

Send multiple inference requests in parallel without waiting for individual completions

🐻Polars Integration

Leverages efficient Polars dataframe operations for request management

💬Multi-turn Conversations

Supports context-preserving conversations across multiple message exchanges

🔌Multiple Providers

Connects with OpenAI, Anthropic, Gemini, Groq, AWS Bedrock — and now on-device MLX

Installation

Using pip

bash
pip install polar-llama==0.5.0

Local MLX extra

Apple Silicon only, Python ≥ 3.10 — installs mlx + mlx-lm for the in-process local backend:

bash
pip install "polar-llama[local]"

Development Installation

bash
maturin develop

Quick Start

Get started with a simple example

python
import polars as pl
from polar_llama import Provider
import dotenv

dotenv.load_dotenv()

# Example questions
questions = [
    'What is the capital of France?',
    'What is the difference between polars and pandas?'
]

df = pl.DataFrame({'Questions': questions})

# Using the fluent .llama namespace (recommended)
df = df.with_columns(
    answer=pl.col('Questions').llama.inference_async(
        provider=Provider.OPENAI,
        model='gpt-4o-mini'
    )
)

print(df)

What's New in 0.5.0

Local MLX Inference Backend

col(...).llama.inference_local(...) — run generation on-device on Apple Silicon, no provider API, no keys, no network

The headline feature of 0.5.0: a local inference backend built on mlx-lm, exposed through the same .llama namespace you already use for OpenAI, Anthropic, Gemini, Groq, and Bedrock. There are two selectable engines:

🌐engine="server" (default)

Points the existing async Rust fan-out at a local OpenAI-compatible endpoint (mlx_lm.server, vllm-mlx) via OPENAI_BASE_URL — no Rust changes, inherits all existing concurrency and error handling

🧠engine="in_process"

A map_batches UDF wrapping mlx-lm's BatchGenerator directly in the Python process, behind a LocalEngine protocol with a FakeEngine test seam. Optional extra: pip install polar-llama[local]

Basic usage

python
import polars as pl
import polar_llama  # registers the .llama namespace

df = pl.DataFrame({"ticket": [
    "My laptop won't turn on even when it's plugged in.",
    "I forgot my password and I'm locked out of my account.",
]})

tagged = df.with_columns(
    tag=pl.col("ticket").llama.inference_local(
        model="mlx-community/gemma-3n-E4B-it-lm-4bit",
        system="Classify the ticket as exactly one of: Hardware, Account, Billing.",
        engine="in_process",   # on-device mlx-lm, batched over the column
        max_tokens=16,
        temperature=0.0,
    )
)

Collapsed Prefix Prefill

10.36× faster prefill on shared prompts, at exact greedy parity

When rows in a batch share a long common prefix — a shared system prompt, or few-shot demonstrations — the in-process engine normally re-prefills that identical prefix for every row. Collapsed prefill (polar_llama/local/collapsed_prefill.py) computes the shared token-level longest-common-prefix once and batches only the per-row suffixes. Measured 10.36× vs sequential on gemma-3n E4B (32 rows, 5 KB shared prompt) at 32/32 exact greedy parity. It falls back to the plain batch_generate path automatically when the shared prefix is short, so it's safe to leave on.

Batched Quantized KV Cache

~47% KV-memory cut at fp16 parity, opt-in via POLAR_LLAMA_LOCAL_KV_BITS=4

BatchQuantizedKVCache closes mlx-lm's "quantized KV × batching" gap. Setting POLAR_LLAMA_LOCAL_KV_BITS=4 cuts KV memory by roughly 47% at parity with fp16 batched output, roughly doubling the batch size or context length that fits in 24 GB — a memory/capacity win rather than a throughput speedup with the current unfused attention path.

bash
POLAR_LLAMA_LOCAL_KV_BITS=4 python tag_column.py

mlx-lm #1384 Batched-RoPE Fix

Runtime monkeypatch correcting garbled batched generation on hybrid Gemma models

A runtime monkeypatch (polar_llama/local/_mlx_patches.py) corrects a RoPE offset-aliasing bug in mlx-lm that garbled batched generation on hybrid Gemma 3n / Gemma 4 models — verified token-identical to sequential generation. Applied automatically whenever a Gemma 3n or Gemma 4 model is loaded through inference_local. A ready-to-post upstream PR lives in patches/PR_1384.md.

Local MLX Backend: Choosing an Engine

Start with engine="server" unless you need the lowest latency for a single Mac with no server process to manage

engine="server" (default)engine="in_process"
How it runsExisting async Rust fan-out talks HTTP to a local OpenAI-compatible server you start yourselfA Python map_batches UDF drives mlx-lm directly in the same process
RequiresAny local server implementing /v1/chat/completions (mlx_lm.server, vllm-mlx, llama.cpp server, LM Studio)pip install polar-llama[local] (mlx + mlx-lm) and an Apple Silicon Mac
ConcurrencyRust futures::buffered, same POLAR_LLAMA_MAX_CONCURRENCY knob as hosted providersmlx-lm's own continuous-batching scheduler (BatchGenerator)
MaturityLow risk — no new code path, just a different base URLNewer, more moving parts — treat as experimental

engine="server": start a local server

Ships with mlx-lm, Apple Silicon only:

bash
pip install mlx-lm
mlx_lm.server --model mlx-community/gemma-4-e2b-it-4bit --port 8080

Point Polar Llama at it

python
import polars as pl

df = pl.DataFrame({"prompt": ["Summarize photosynthesis in one sentence."]})

# Option A: pass base_url explicitly
df = df.with_columns(
    answer=pl.col("prompt").llama.inference_local(
        model="mlx-community/gemma-4-e2b-it-4bit",
        engine="server",
        base_url="http://localhost:8080",
    )
)

# Option B: export OPENAI_BASE_URL and omit base_url —
# inference_local picks it up the same way any OpenAI-routed call does.

engine="in_process": batched generation via mlx-lm

Continuous batching (BatchGenerator) and batched, left-padded KV cache (BatchKVCache / BatchRotatingKVCache) — Polar Llama doesn't reimplement either:

python
import polars as pl

df = pl.DataFrame({"prompt": [
    "Explain the sliding-window trick in one sentence.",
    "What is a KV cache?",
]})

df = df.with_columns(
    answer=pl.col("prompt").llama.inference_local(
        model="mlx-community/gemma-4-e2b-it-4bit",
        engine="in_process",
        max_tokens=256,
    )
)

The loaded model is held in a process-global singleton behind a lock, keyed by (model, engine), so repeated calls reuse the same weights and cache instead of reloading per batch. The result column is String completions in the original row order — the same contract as every other inference_* function in the library.

Examples & Cookbooks

Tool Use: Calorie Tracker

Emit tool calls as structured output, execute them batch-parallel, then synthesize a summary — three explicit turns, each an ordinary column

python
import json
import dotenv
import polars as pl
from pydantic import BaseModel

from polar_llama import (
    Provider,
    combine_messages,
    execute_tool_calls,
    inference_messages,
    tool_results_to_message,
    tools_to_response_model,
)

dotenv.load_dotenv()

TOOLS = [
    {
        "name": "search_food_db",
        "description": "Search a nutrition database for one food item and portion.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "The food item, singular"},
                "portion": {"type": "string", "description": "Portion as stated, e.g. 'two eggs'"},
            },
            "required": ["query", "portion"],
        },
    }
]

FAKE_DB = {
    "egg": 78, "sourdough toast": 120, "butter": 102, "black coffee": 2,
    "chicken bowl": 510, "guacamole": 230, "rice": 200,
}

def nutrition_executor(tool_name: str, arguments: dict):
    query = arguments["query"].lower()
    for food, calories in FAKE_DB.items():
        if food in query or query in food:
            return json.dumps({"food": food, "calories_per_serving": calories,
                               "portion": arguments["portion"]})
    return json.dumps({"food": query, "error": "not found"}), True

class NutritionSummary(BaseModel):
    total_calories: int
    items: list[str]
    notes: str

meals = pl.DataFrame({"meal": [
    "two eggs, sourdough toast with butter, black coffee",
    "chipotle chicken bowl, no rice, extra guac",
]})

FoodSearches = tools_to_response_model(TOOLS)

result = (
    meals
    # Turn 1a: the LLM parameterizes N searches per row (structured output).
    .with_columns(
        calls=pl.col("meal").llama.inference_async(
            provider=Provider.OPENAI, model="gpt-4o-mini",
            response_model=FoodSearches,
        )
    )
    # Turn 1b: all searches across all rows execute concurrently.
    .with_columns(
        results=execute_tool_calls(pl.col("calls"), executor=nutrition_executor, tools=TOOLS)
    )
    # Turn 2: reconcile results into a typed summary.
    .with_columns(
        nutrition=inference_messages(
            combine_messages(
                pl.col("meal").llama.to_message(role="user"),
                tool_results_to_message(pl.col("results")),
            ),
            provider=Provider.OPENAI, model="gpt-4o-mini",
            response_model=NutritionSummary,
        )
    )
)

print(result.select("meal", "nutrition"))

Prompt Optimization: Bootstrapped Few-Shot + Instruction Search

DSPy-style Signature / Predict / BootstrapFewShot / InstructionOptimizer against a labeled DataFrame

python
import polars as pl
from polar_llama import Predict, Signature, BootstrapFewShot, InstructionOptimizer, evaluate

# 1. Declare the task
module = Predict(
    Signature("question -> answer", instructions="Answer concisely."),
    provider="openai",
    model="gpt-4o-mini",
)

# 2. Labeled training data
trainset = pl.DataFrame({
    "question": ["What is 2+2?", "Capital of France?", "Largest planet?"],
    "answer": ["4", "Paris", "Jupiter"],
})

# 3. A metric: (gold row, prediction) -> bool | float
def exact_match(example, prediction):
    return example["answer"].strip().lower() == (prediction["answer"] or "").strip().lower()

# 4a. Bootstrap few-shot demos from rows the model already gets right
compiled = BootstrapFewShot(metric=exact_match, max_demos=4).compile(module, trainset)

# 4b. Or search for better instructions (COPRO-style)
optimizer = InstructionOptimizer(metric=exact_match, n_candidates=4)
compiled = optimizer.compile(module, trainset)
print(optimizer.history)  # [(instructions, score), ...]

# 5. Run the optimized module on new data — outputs land in pred_* columns
result = compiled(pl.DataFrame({"question": ["What is 3+3?"]}))
print(result["pred_answer"])

# Score any module against a labeled set
print(evaluate(compiled, trainset, exact_match).score)

Multi-Message Conversations

Maintain context across multiple messages for more natural interactions

python
import polars as pl
from polar_llama import combine_messages, inference_messages
import dotenv

dotenv.load_dotenv()

df = pl.DataFrame({
    "system_prompt": [
        "You are a helpful assistant.",
        "You are a math expert."
    ],
    "user_question": [
        "What's the weather like today?",
        "Solve x^2 + 5x + 6 = 0"
    ]
})

# Using .llama namespace (recommended)
df = df.with_columns([
    pl.col("system_prompt").llama.to_message(role="system").alias("system_message"),
    pl.col("user_question").llama.to_message(role="user").alias("user_message")
])

df = df.with_columns(
    conversation=combine_messages(pl.col("system_message"), pl.col("user_message"))
)

df = df.with_columns(
    response=inference_messages(pl.col("conversation"), provider="openai", model="gpt-4o")
)

print(df.select(["user_question", "response"]))

Data Analysis Pipeline

Process customer feedback at scale

python
import polars as pl
from polar_llama import Provider

feedback_df = pl.DataFrame({
    'customer_id': [101, 102, 103, 104, 105],
    'feedback': [
        'The product is amazing but shipping was slow',
        'Great quality, highly recommend!',
        'Disappointed with customer service',
        'Perfect for my needs, will buy again',
        'Product arrived damaged, requesting refund'
    ]
})

sentiment_prompt = """Analyze the sentiment of this customer feedback
and classify it as Positive, Negative, or Neutral.
Also provide a brief reason.

Feedback: {feedback}"""

df = feedback_df.with_columns(
    prompt=pl.format(sentiment_prompt, pl.col('feedback'))
)

df = df.with_columns(
    sentiment_analysis=pl.col('prompt').llama.inference_async(
        provider=Provider.OPENAI,
        model='gpt-4o-mini'
    )
)

print(df.select(['customer_id', 'feedback', 'sentiment_analysis']))

Provider Support

Polar Llama supports multiple LLM providers, plus on-device MLX

OpenAI

Default model: gpt-4o-mini

python
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_async(
        provider=Provider.OPENAI,
        model='gpt-4o-mini'
    )
)

Anthropic (Claude)

Default model: claude-opus-4-8

python
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_async(
        provider=Provider.ANTHROPIC,
        model='claude-opus-4-8'
    )
)

Google Gemini

Default model: gemini-2.5-flash. Native system_instruction support and native JSON-schema structured outputs (response_json_schema).

python
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_async(
        provider=Provider.GEMINI,
        model='gemini-2.5-flash'
    )
)

Groq

Default model: llama-3.3-70b-versatile

python
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_async(
        provider=Provider.GROQ,
        model='llama-3.3-70b-versatile'
    )
)

AWS Bedrock

Default model: us.anthropic.claude-haiku-4-5-20251001-v1:0. Requires AWS credentials configured; region resolves from AWS_REGION / AWS_DEFAULT_REGION before falling back to us-east-1.

python
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_async(
        provider='bedrock',
        model='us.anthropic.claude-haiku-4-5-20251001-v1:0'
    )
)

Local MLX (Apple Silicon)

New in 0.5.0 — no provider API, runs entirely on-device via mlx-lm

python
df = df.with_columns(
    answer=pl.col('prompt').llama.inference_local(
        model='mlx-community/gemma-4-e2b-it-4bit',
        engine='in_process',
        max_tokens=256,
    )
)

Advanced Features

Environment Configuration

Set up your API keys and overrides in a .env file:

.envbash
OPENAI_API_KEY=your_openai_key
ANTHROPIC_API_KEY=your_anthropic_key
GEMINI_API_KEY=your_gemini_key
GROQ_API_KEY=your_groq_key
AWS_ACCESS_KEY_ID=your_aws_key
AWS_SECRET_ACCESS_KEY=your_aws_secret
AWS_REGION=us-east-1

# Proxies / gateways (added 0.3.0)
OPENAI_BASE_URL=https://your-proxy.example.com/v1
ANTHROPIC_BASE_URL=https://your-proxy.example.com

# Bound concurrent in-flight requests per batch (default 64)
POLAR_LLAMA_MAX_CONCURRENCY=64

# Local MLX backend (Apple Silicon, added 0.5.0)
POLAR_LLAMA_LOCAL_KV_BITS=4      # quantized KV cache, opt-in

Prompt Caching

Provider-native prompt caching shares a cached system prefix across rows (Anthropic cache_control, 5m/1h TTL):

python
from polar_llama import CacheConfig, CacheStrategy

df = df.with_columns(
    answer=pl.col('prompt').llama.inference_async(
        provider=Provider.ANTHROPIC,
        model='claude-opus-4-8',
        system_prompt="You are a careful, concise research assistant.",
        cache=True,   # or pass a CacheConfig for fine-grained control
    )
)

config = CacheConfig(
    strategy=CacheStrategy.AUTO,
    min_tokens=1024,
    ttl="5m",   # or "1h" (extended-cache-ttl beta)
)

Testing

Run tests with configured providers:

bash
pip install -r tests/requirements.txt
pytest tests/ -v
cargo test --test model_client_tests -- --nocapture

Changelog Highlights (post-0.2.2 → 0.5.0)

  • 0.5.0 — local MLX inference backend (inference_local), collapsed prefix prefill, batched quantized KV cache, and the mlx-lm #1384 batched-RoPE fix for hybrid Gemma models.
  • 0.3.0 — tool use / MCP integration (tools_to_response_model, execute_tool_calls), provider-native prompt caching, a DSPy-style prompt optimization engine (Signature, Predict, BootstrapFewShot, InstructionOptimizer), OPENAI_BASE_URL / ANTHROPIC_BASE_URL proxy support, and POLAR_LLAMA_MAX_CONCURRENCY.
  • 0.3.0 — updated default models across all providers (retired models like gpt-4-turbo and claude-3-opus-20240229 replaced), fixed Gemini and Bedrock structured-output auth, removed disabled TLS verification, and a significant performance pass (shared HTTP client, bounded concurrency, cached Bedrock credentials).
  • 0.2.2 — LLM cost calculation, vector embeddings (embedding_async), similarity functions (cosine_similarity, dot_product, euclidean_distance), and HNSW approximate nearest neighbor search (knn_hnsw).

Upcoming Features

  • Streaming response capabilities
  • Broader non-Mac local-server coverage for the in-process engine's feature set

API Surface

Notable functions and the .llama namespace methods available as of 0.5.0

Function / MethodPurpose
.llama.inference_local(model=..., engine=..., ...)Local MLX generation, new in 0.5.0
mcp_tools(transport)Fetch tool definitions from an MCP server (tools/list)
tools_to_response_model(tools)Build the structured-output emission schema for tool calls
execute_tool_calls(expr, transport=... | executor=...)Run every emitted call of every row in parallel; failures are data
tool_results_to_message(expr)Render a results column as a message for the synthesis turn
Signature / Predict / evaluateDeclare a task and run one batched inference per DataFrame
BootstrapFewShot / InstructionOptimizerMine few-shot demos / search instructions against labeled data
embedding_async(expr, provider=..., model=...)Parallel embedding generation
cosine_similarity / dot_product / euclidean_distanceRust-powered vector similarity metrics
knn_hnsw(query, corpus, k=...)HNSW approximate nearest neighbor search
tag_taxonomy(expr, taxonomy, ...)Taxonomy-based classification with reasoning and confidence

Common Use Cases

📊Data Analysis

Process large datasets with AI insights — sentiment analysis, classification, entity extraction with validated structured outputs

🤖Agentic Tool Use

Batch-parallel tool-call emission and execution against an MCP server or Python callable, with every intermediate step an inspectable column

🖥️Offline / On-Device Pipelines

Run classification, tagging, or prompt tuning entirely on Apple Silicon with no API keys and no network dependency

🔍Semantic Search

Generate embeddings and run HNSW nearest-neighbor search combined with taxonomy filtering for precise, context-aware retrieval

Resources
GitHub RepositoryPyPI PackagePolars Documentation

Licensed under MIT.

Questions or issues? Open one on GitHub.