Open source library

Polar Llama

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

v0.5.1Released July 5, 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.1. 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.1 bridges the DSPy-style prompt optimizer onto the on-device MLX backend introduced in 0.5.0 — tuning instructions and few-shot demos against a local Gemma model, with no cloud calls and no Rust path involved.

Concurrent Processing

Send multiple inference requests in parallel without waiting for individual completions

🐻Polars Integration

Leverages efficient Polars dataframe operations for request management

🎛️Prompt Optimization

Signature / Predict / BootstrapFewShot / InstructionOptimizer — now tunable entirely on-device

🔌Multiple Providers

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

Installation

Using pip

bash
pip install polar-llama==0.5.1

Local MLX extra

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

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.1

Local Prompt-Tuning Bridge

polar_llama.local.make_local_inference_fn(model, ...) — drive the optimizer with on-device gemma-3n, no cloud or Rust path

make_local_inference_fn returns an inference_fn that drives polar_llama.optimize's DSPy-style Predict / BootstrapFewShot / InstructionOptimizer against on-device gemma-3n (mlx-lm). It reuses the same singleton-loaded weights as inference_local(engine="in_process") and applies the mlx-lm #1384 batched fix automatically.

python
from polar_llama.local import make_local_inference_fn
from polar_llama import Predict, Signature, InstructionOptimizer

# Backs Predict/BootstrapFewShot/InstructionOptimizer with on-device gemma-3n
# (mlx-lm). Requires the [local] extra.
fn = make_local_inference_fn("mlx-community/gemma-3n-E4B-it-lm-4bit")
module = Predict(Signature("note -> category", instructions="Classify the note."), inference_fn=fn)

def exact_match(example, prediction):
    return example["category"] == prediction["category"]

compiled = InstructionOptimizer(metric=exact_match).compile(module, trainset)

Collapsed Prefill for inference_local

POLAR_LLAMA_LOCAL_COLLAPSE=1 — ~2.8× faster on a full prompt-tuning schedule, at identical output

The collapsed-prefill primitive introduced in 0.5.0 (polar_llama/local/collapsed_prefill.py) is now exposed as an opt-in flag on inference_local(engine="in_process") itself — set POLAR_LLAMA_LOCAL_COLLAPSE=1 to share the common prompt prefix across rows (this is also the prompt-tuning bridge's default). Measured ~2.8× faster on a full prompt-tuning schedule (3.4× on a demo-laden eval) at identical, parity-verified output — the dominant speedup whenever rows share a long prefix, such as a shared system prompt or few-shot demos during tuning. Mutually exclusive with POLAR_LLAMA_LOCAL_KV_BITS — the quantized-KV path takes precedence when both are set.

bash
POLAR_LLAMA_LOCAL_COLLAPSE=1 python tag_column.py

Singleton Weight Reuse

MlxBatchEngine.get_model_and_tokenizer() reuses already-loaded weights instead of reloading per call

The prompt-tuning bridge and inference_local now share one code path for accessing the loaded model and tokenizer, so repeated Predict calls during an optimization run don't re-trigger a model load.

Fixed: InstructionOptimizer crash on array-valued instructions

TypeError: the truth value of a Series is ambiguous

InstructionOptimizer no longer crashes when the proposer model returns the instructions field as a JSON array instead of a newline-delimited string — list/Series values are now flattened to newline-delimited text (polar_llama/optimize.py). This surfaces with small local models that emit {"instructions": [...]}.

Examples & Cookbooks

On-Device Prompt Optimization

Drive the optimizer with a local model via make_local_inference_fn — no API keys, no network

python
from polar_llama.local import make_local_inference_fn
from polar_llama import Predict, Signature, InstructionOptimizer

# Backs Predict/BootstrapFewShot/InstructionOptimizer with on-device gemma-3n
# (mlx-lm). Collapsed prefill (on by default) shares the system+demos prefix
# across rows — the dominant speedup during tuning, where few-shot demos make
# that prefix ~80% of every prompt (~2.8x on a full tuning schedule, at
# identical output). Requires the [local] extra.
fn = make_local_inference_fn("mlx-community/gemma-3n-E4B-it-lm-4bit")
module = Predict(Signature("note -> category", instructions="Classify the note."), inference_fn=fn)

def exact_match(example, prediction):
    return example["category"] == prediction["category"]

compiled = InstructionOptimizer(metric=exact_match).compile(module, trainset)

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"))

On-Device Inference (in-process engine)

Batched generation via mlx-lm, no API keys, no network

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,
    )
)

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)

No provider API, runs entirely on-device via mlx-lm; the prompt-tuning bridge shares its weights

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)
POLAR_LLAMA_LOCAL_KV_BITS=4         # quantized KV cache, opt-in (0.5.0+)
POLAR_LLAMA_LOCAL_COLLAPSE=1        # collapsed prefix prefill for inference_local (new in 0.5.1)

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

# Local MLX backend: CPU-safe logic tests only (no GPU, no mlx import)
pytest tests/ -m "not local_gpu"

Changelog Highlights (post-0.2.2 → 0.5.1)

  • 0.5.1 — local prompt-tuning bridge (make_local_inference_fn), collapsed-prefill opt-in for inference_local (POLAR_LLAMA_LOCAL_COLLAPSE=1), singleton weight reuse, and an InstructionOptimizer fix for array-valued instructions.
  • 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.1

Function / MethodPurpose
.llama.inference_local(model=..., engine=..., ...)Local MLX generation (0.5.0)
polar_llama.local.make_local_inference_fn(model, ...)Bridge the optimizer to on-device MLX, new in 0.5.1
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 Prompt Tuning

Bootstrap few-shot demos or search for better instructions entirely on-device via make_local_inference_fn — no API keys, no network

🔍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.