By Sagar Shankaran, Founder of CallSphere
Learn how prefix tuning and soft prompts let you customize LLM behavior by training small continuous vectors prepended to model inputs, achieving fine-tuning-level performance at a fraction of the cost.
Key takeaways
Traditional prompting writes instructions in natural language — these are "hard" prompts made of discrete tokens from the model's vocabulary. But natural language is a lossy, imprecise interface. You are limited to what can be expressed in words, and the model interprets your instructions through the lens of its training data.
Prefix tuning takes a radically different approach: instead of searching for the right words, it learns continuous vectors (soft prompts) that are prepended to the model's hidden states. These vectors exist in the model's continuous embedding space, not in the vocabulary space, so they can represent instructions that no natural language string could express.
In prefix tuning, you prepend a sequence of trainable vectors to the key and value matrices in every attention layer of the transformer. The original model parameters are completely frozen — only the prefix vectors are updated during training.
flowchart LR
DATA[("Curated dataset<br/>instruction or chat")]
CLEAN["Clean and dedupe<br/>PII filter"]
TOK["Tokenize and pack"]
METHOD{"Method"}
LORA["LoRA or QLoRA<br/>adapters only"]
SFT["Full SFT<br/>all params"]
DPO["DPO or RLHF<br/>preference learning"]
EVAL["Held out eval<br/>plus regression suite"]
DEPLOY[("Adapter or<br/>merged model")]
DATA --> CLEAN --> TOK --> METHOD
METHOD --> LORA --> EVAL
METHOD --> SFT --> EVAL
METHOD --> DPO --> EVAL
EVAL --> DEPLOY
style METHOD fill:#4f46e5,stroke:#4338ca,color:#fff
style EVAL fill:#f59e0b,stroke:#d97706,color:#1f2937
style DEPLOY fill:#059669,stroke:#047857,color:#fff
import torch
import torch.nn as nn
from transformers import AutoModelForCausalLM, AutoTokenizer
class PrefixTuningWrapper(nn.Module):
"""Wraps a frozen LLM with trainable prefix vectors."""
def __init__(self, model_name: str, prefix_length: int = 20, prefix_dim: int = 512):
super().__init__()
self.model = AutoModelForCausalLM.from_pretrained(model_name)
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
# Freeze all model parameters
for param in self.model.parameters():
param.requires_grad = False
config = self.model.config
self.num_layers = config.num_hidden_layers
self.num_heads = config.num_attention_heads
self.head_dim = config.hidden_size // config.num_attention_heads
self.prefix_length = prefix_length
# Trainable prefix embeddings + reparameterization MLP
self.prefix_embedding = nn.Embedding(prefix_length, prefix_dim)
self.prefix_mlp = nn.Sequential(
nn.Linear(prefix_dim, prefix_dim),
nn.Tanh(),
nn.Linear(prefix_dim, self.num_layers * 2 * config.hidden_size),
)
def get_prefix(self, batch_size: int) -> list[tuple[torch.Tensor, torch.Tensor]]:
"""Generate prefix key-value pairs for all layers."""
prefix_ids = torch.arange(self.prefix_length).unsqueeze(0).expand(batch_size, -1)
prefix_emb = self.prefix_embedding(prefix_ids)
past_key_values = self.prefix_mlp(prefix_emb)
# Reshape into per-layer key-value pairs
past_key_values = past_key_values.view(
batch_size, self.prefix_length, self.num_layers, 2,
self.num_heads, self.head_dim,
)
past_key_values = past_key_values.permute(2, 3, 0, 4, 1, 5)
return [(kv[0], kv[1]) for kv in past_key_values]
def forward(self, input_ids, attention_mask=None):
batch_size = input_ids.shape[0]
past_key_values = self.get_prefix(batch_size)
# Extend attention mask for prefix tokens
prefix_mask = torch.ones(batch_size, self.prefix_length, device=input_ids.device)
if attention_mask is not None:
attention_mask = torch.cat([prefix_mask, attention_mask], dim=1)
return self.model(
input_ids=input_ids,
attention_mask=attention_mask,
past_key_values=past_key_values,
)
Training is straightforward: define a task-specific dataset, compute the loss using the frozen model's outputs, and backpropagate only through the prefix parameters. Because you are training only a few thousand parameters instead of billions, training is fast and requires minimal GPU memory.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
from torch.utils.data import DataLoader
from transformers import get_linear_schedule_with_warmup
def train_prefix(
wrapper: PrefixTuningWrapper,
train_dataset,
epochs: int = 5,
lr: float = 1e-3,
batch_size: int = 8,
):
"""Train prefix vectors on a task-specific dataset."""
# Only optimize prefix parameters
optimizer = torch.optim.AdamW(
[p for p in wrapper.parameters() if p.requires_grad],
lr=lr,
)
dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
scheduler = get_linear_schedule_with_warmup(
optimizer, num_warmup_steps=100, num_training_steps=len(dataloader) * epochs,
)
wrapper.train()
for epoch in range(epochs):
total_loss = 0
for batch in dataloader:
outputs = wrapper(batch["input_ids"], batch["attention_mask"])
loss = outputs.loss
loss.backward()
optimizer.step()
scheduler.step()
optimizer.zero_grad()
total_loss += loss.item()
print(f"Epoch {epoch + 1}, Loss: {total_loss / len(dataloader):.4f}")
return wrapper
Both are parameter-efficient fine-tuning (PEFT) methods, but they work differently:
Prefix tuning adds trainable vectors to the input of attention layers. It modifies what the model "sees" without changing its internal weights. Trained prefix vectors are tiny (often under 1MB) and can be swapped at inference time.
LoRA adds low-rank decomposition matrices to the model's weight matrices. It modifies how the model processes information. LoRA adapters are larger (10-100MB) but often achieve higher task performance because they directly modify the model's computations.
For agent developers, prefix tuning's advantage is its extreme efficiency in multi-tenant scenarios. You can store thousands of task-specific prefixes and swap them per request without reloading the model.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
In production agent systems, soft prompts enable per-task customization without model replication. A single served model can use different prefix vectors for different agent capabilities:
class MultiTaskAgent:
"""Agent that switches prefix vectors based on the current task."""
def __init__(self, base_model, prefix_store: dict[str, torch.Tensor]):
self.model = base_model
self.prefix_store = prefix_store # {"summarize": tensor, "classify": tensor, ...}
def run(self, task_type: str, user_input: str) -> str:
prefix = self.prefix_store.get(task_type)
if prefix is None:
raise ValueError(f"No prefix trained for task: {task_type}")
# Apply task-specific prefix and generate
return self.model.generate_with_prefix(prefix, user_input)
Prefix tuning is surprisingly data-efficient. Good results can often be achieved with as few as 500-1000 task-specific examples. For simple classification or format control tasks, even 100-200 examples may suffice. The key is that examples should be representative of the actual distribution your agent will encounter.
Yes. In practice, you can apply LoRA to the model weights for broad domain adaptation and then add prefix tuning for task-specific behavior. The PEFT library from Hugging Face supports combining multiple adapter types on the same base model.
No. Prefix tuning requires injecting continuous vectors into the model's internal hidden states, which is only possible with local models where you control the inference pipeline. For API-based models, prompt engineering and fine-tuning APIs (where available) are the alternatives.
#PrefixTuning #SoftPrompts #ParameterEfficientFineTuning #PEFT #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
The 2026 desktop AI agent landscape — ServiceNow Project Arc, Anthropic Claude offerings, OpenAI agents, and Google Mariner. A buyer's map.
An agentic-AI perspective on Anthropic Skills system, covering orchestration patterns, tool use, and how agent tooling fits production agent stacks.
Enterprise CIO Guide perspective on Comet's general-availability launch put an agentic browser in front of millions of consumers, and it works better than the demos suggested.
Enterprise CIO Guide perspective on Harvey AI's enterprise rollout numbers show legal agents have moved past the pilot stage at AmLaw 100 firms.
Enterprise CIO Guide perspective on Hippocratic AI's deployment numbers show healthcare voice agents are moving from pilot to production across major US health systems.
An agentic-AI perspective on Claude Agent SDK loops, covering orchestration patterns, tool use, and how agent orchestration fits production agent stacks.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco