Beyond Frontier Models: A Technical Deep Dive & Practitioner's Guide to LLM Tuning, Optimization, Deployment, and Architecture
This article was originally published on LinkedIn.
Disclaimer: All opinions expressed in this post are my own and do not represent the views or positions of my employer.
The most effective LLM systems aren’t built by treating training, deployment, and evaluation as isolated steps. They emerge from understanding how these components interconnect. In this guide, I’ll share the technical details that matter for building production-grade LLM applications.
Part 1: Technical Foundations of LLM Post-Training
Post-training transforms a general-purpose language model into something useful for your specific domain. There are three key phases.
Phase 1: Supervised Fine-Tuning (SFT)
SFT converts a raw model into an instruction follower using curated instruction-response pairs.
# Training data format for instruction tuning
training_examples = [
{
"instruction": "Summarize the following article in 3 bullet points.",
"input": "<article text>",
"output": "• Key point 1\n• Key point 2\n• Key point 3"
},
# Diverse phrasing matters!
{
"instruction": "Give me a 3-point summary of this text.",
"input": "<article text>",
"output": "..."
}
]
# SFT training loop
def sft_training_step(model, batch, optimizer):
inputs = tokenize(batch["instruction"] + batch["input"])
targets = tokenize(batch["output"])
logits = model(inputs)
loss = cross_entropy_loss(logits, targets)
loss.backward()
optimizer.step()
return loss
Key considerations:
- Diverse instruction phrasing prevents overfitting to specific formats
- Quality over quantity—1000 excellent examples beat 100,000 mediocre ones
- Include edge cases and failure modes in training data
Phase 2: Preference Optimization (RLHF, DPO, RLAIF)
Preference optimization teaches models to generate responses humans prefer.
# Direct Preference Optimization (DPO) - simpler than RLHF
def dpo_loss(model, ref_model, preferred, rejected, beta=0.1):
"""
DPO loss without explicit reward modeling.
beta controls deviation from reference model.
"""
# Log probabilities under current and reference models
pi_preferred = model.log_prob(preferred)
pi_rejected = model.log_prob(rejected)
ref_preferred = ref_model.log_prob(preferred)
ref_rejected = ref_model.log_prob(rejected)
# DPO objective
preferred_ratio = pi_preferred - ref_preferred
rejected_ratio = pi_rejected - ref_rejected
loss = -torch.log(torch.sigmoid(beta * (preferred_ratio - rejected_ratio)))
return loss.mean()
When to use what:
- RLHF: Maximum control, but complex (requires reward model + PPO)
- DPO: Simpler, often comparable results
- RLAIF: When human feedback is expensive, use AI feedback
Phase 3: Domain-Adaptive Continued Pre-training
For specialized domains (legal, medical, finance), continued pre-training on domain corpora bridges the knowledge gap.
# Domain adaptation strategy
domain_training_config = {
"learning_rate": 1e-5, # Lower than initial pre-training
"warmup_ratio": 0.1,
"epochs": 2, # Don't overtrain - causes forgetting
"data_mix": {
"domain_specific": 0.7,
"general": 0.3 # Prevents catastrophic forgetting
}
}
Loss Functions for Different Objectives
# Cross-Entropy: Standard next-token prediction
def cross_entropy_loss(logits, targets):
return F.cross_entropy(logits.view(-1, vocab_size), targets.view(-1))
# Triplet Loss: For embedding training
def triplet_loss(anchor, positive, negative, margin=0.2):
pos_dist = F.pairwise_distance(anchor, positive)
neg_dist = F.pairwise_distance(anchor, negative)
return F.relu(pos_dist - neg_dist + margin).mean()
# KL Divergence: For distillation or regularization
def kl_divergence_loss(student_logits, teacher_logits, temperature=2.0):
student_probs = F.log_softmax(student_logits / temperature, dim=-1)
teacher_probs = F.softmax(teacher_logits / temperature, dim=-1)
return F.kl_div(student_probs, teacher_probs, reduction='batchmean')
# Contrastive Loss (InfoNCE): For embedding models
def contrastive_loss(embeddings, temperature=0.07):
similarity = embeddings @ embeddings.T / temperature
labels = torch.arange(len(embeddings))
return F.cross_entropy(similarity, labels)
Parameter-Efficient Fine-Tuning (PEFT)
Full fine-tuning is expensive. PEFT methods update only a fraction of parameters.
LoRA (Low-Rank Adaptation)
class LoRALayer(nn.Module):
"""
LoRA: Learns low-rank updates to frozen weights.
W_new = W_frozen + (A @ B) * scaling
"""
def __init__(self, in_features, out_features, rank=8, alpha=16):
super().__init__()
self.rank = rank
self.scaling = alpha / rank
# Frozen original weights
self.weight = nn.Parameter(torch.randn(out_features, in_features),
requires_grad=False)
# Trainable low-rank matrices
self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
def forward(self, x):
# Original forward + low-rank update
original = F.linear(x, self.weight)
lora_update = F.linear(F.linear(x, self.lora_A), self.lora_B)
return original + lora_update * self.scaling
QLoRA: Quantization + LoRA
# QLoRA configuration for memory-efficient fine-tuning
qlora_config = {
"load_in_4bit": True,
"bnb_4bit_quant_type": "nf4", # NormalFloat4 - optimal for weights
"bnb_4bit_compute_dtype": torch.bfloat16,
"bnb_4bit_use_double_quant": True, # Quantize the quantization constants
"lora_r": 64,
"lora_alpha": 16,
"lora_dropout": 0.1,
"target_modules": ["q_proj", "k_proj", "v_proj", "o_proj"]
}
Memory comparison for 7B model:
- Full fine-tuning: ~60GB VRAM
- LoRA (r=8): ~16GB VRAM
- QLoRA (4-bit + LoRA): ~6GB VRAM
Part 2: Production Deployment Architecture
Inference Parameters That Matter
generation_config = {
# Temperature: Controls randomness
# 0.0-0.3: Factual, deterministic tasks
# 0.7-1.0: Creative tasks
"temperature": 0.7,
# Top-p (nucleus sampling): Dynamic vocabulary filtering
# Samples from smallest set with cumulative probability >= p
"top_p": 0.9,
# Top-k: Fixed vocabulary limit
"top_k": 50,
# Repetition control
"presence_penalty": 0.6, # Penalize tokens that appeared
"frequency_penalty": 0.3, # Penalize based on frequency
}
def generate_with_config(model, prompt, config):
logits = model(prompt)
# Apply temperature
logits = logits / config["temperature"]
# Apply top-k filtering
if config["top_k"] > 0:
indices_to_remove = logits < torch.topk(logits, config["top_k"])[0][..., -1, None]
logits[indices_to_remove] = float('-inf')
# Apply top-p filtering
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
sorted_indices_to_remove = cumulative_probs > config["top_p"]
sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
sorted_indices_to_remove[..., 0] = 0
indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
logits[indices_to_remove] = float('-inf')
# Sample
probs = F.softmax(logits, dim=-1)
return torch.multinomial(probs, num_samples=1)
Hardware & Memory Considerations
def calculate_memory_requirements(
num_params_billions,
precision="fp16",
batch_size=1,
sequence_length=2048
):
"""Estimate GPU memory for inference."""
bytes_per_param = {
"fp32": 4,
"fp16": 2,
"bf16": 2,
"int8": 1,
"int4": 0.5
}
# Model weights
model_memory_gb = (num_params_billions * 1e9 *
bytes_per_param[precision]) / 1e9
# KV cache (for each layer)
# Assuming ~100 layers, hidden_size ≈ num_params^0.5 * 1000
hidden_size = int((num_params_billions ** 0.5) * 1000)
num_layers = min(int(num_params_billions * 10), 100)
kv_cache_per_token = 2 * num_layers * hidden_size * bytes_per_param[precision]
kv_cache_gb = (batch_size * sequence_length * kv_cache_per_token) / 1e9
# Activation memory (rough estimate)
activation_gb = model_memory_gb * 0.1
return {
"model_weights_gb": model_memory_gb,
"kv_cache_gb": kv_cache_gb,
"activations_gb": activation_gb,
"total_gb": model_memory_gb + kv_cache_gb + activation_gb
}
# Example: 70B model in fp16
# calculate_memory_requirements(70, "fp16") → ~150GB total
# calculate_memory_requirements(70, "int4") → ~45GB total
Inference Optimization Techniques
Speculative Decoding
def speculative_decode(target_model, draft_model, prompt, num_speculative=4):
"""
Use small draft model to propose tokens, verify with target model.
Can achieve 2-3x speedup.
"""
generated = prompt
while not is_complete(generated):
# Draft model generates multiple tokens quickly
draft_tokens = []
draft_probs = []
for _ in range(num_speculative):
logits = draft_model(generated + draft_tokens)
token = sample(logits)
draft_tokens.append(token)
draft_probs.append(F.softmax(logits, dim=-1))
# Target model verifies all at once (single forward pass)
target_logits = target_model(generated + draft_tokens)
target_probs = F.softmax(target_logits, dim=-1)
# Accept tokens where target agrees with draft
accepted = 0
for i, (draft_p, target_p) in enumerate(zip(draft_probs, target_probs)):
# Acceptance probability
r = random.random()
if r < min(1, target_p[draft_tokens[i]] / draft_p[draft_tokens[i]]):
accepted += 1
else:
break
generated += draft_tokens[:accepted]
# Sample one more from target if we rejected early
if accepted < num_speculative:
generated += sample(target_logits[accepted])
return generated
KV Cache Optimization
class PagedKVCache:
"""
vLLM-style paged attention for efficient memory usage.
Allocates memory in pages, enables sharing across requests.
"""
def __init__(self, page_size=16, max_pages=1000):
self.page_size = page_size
self.pages = {} # page_id -> tensor
self.page_table = {} # request_id -> [page_ids]
def allocate_page(self):
page_id = len(self.pages)
self.pages[page_id] = torch.zeros(self.page_size, hidden_size)
return page_id
def get_kv_cache(self, request_id, position):
page_idx = position // self.page_size
offset = position % self.page_size
if request_id not in self.page_table:
self.page_table[request_id] = []
while len(self.page_table[request_id]) <= page_idx:
self.page_table[request_id].append(self.allocate_page())
page_id = self.page_table[request_id][page_idx]
return self.pages[page_id][offset]
Part 3: RAG and Retrieval Strategies
Hybrid Retrieval Architecture
class HybridRetriever:
"""
Combines dense (semantic) and sparse (keyword) retrieval.
"""
def __init__(self, dense_model, sparse_index, dense_index):
self.dense_model = dense_model
self.sparse_index = sparse_index # BM25, TF-IDF
self.dense_index = dense_index # FAISS, Pinecone
def retrieve(self, query, k=10, dense_weight=0.7):
# Dense retrieval
query_embedding = self.dense_model.encode(query)
dense_results = self.dense_index.search(query_embedding, k=k*2)
# Sparse retrieval
sparse_results = self.sparse_index.search(query, k=k*2)
# Reciprocal Rank Fusion
scores = {}
for rank, (doc_id, _) in enumerate(dense_results):
scores[doc_id] = scores.get(doc_id, 0) + dense_weight / (rank + 60)
for rank, (doc_id, _) in enumerate(sparse_results):
scores[doc_id] = scores.get(doc_id, 0) + (1 - dense_weight) / (rank + 60)
# Return top-k by fused score
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return ranked[:k]
Advanced RAG Techniques
# Multi-stage retrieval with reranking
class MultiStageRAG:
def __init__(self, retriever, reranker, generator):
self.retriever = retriever
self.reranker = reranker # Cross-encoder for precise ranking
self.generator = generator
def query(self, question, k_retrieve=50, k_rerank=5):
# Stage 1: Fast retrieval (bi-encoder)
candidates = self.retriever.retrieve(question, k=k_retrieve)
# Stage 2: Precise reranking (cross-encoder)
reranked = self.reranker.rerank(question, candidates, k=k_rerank)
# Stage 3: Generate with retrieved context
context = "\n\n".join([doc.text for doc in reranked])
prompt = f"""Context:\n{context}\n\nQuestion: {question}\n\nAnswer:"""
return self.generator.generate(prompt)
# Query decomposition for complex questions
def decompose_query(question, llm):
"""Break complex questions into sub-questions."""
prompt = f"""Break this question into simpler sub-questions:
Question: {question}
Sub-questions:"""
sub_questions = llm.generate(prompt).split('\n')
return [q.strip() for q in sub_questions if q.strip()]
Part 4: Evaluation & Benchmarking
Comprehensive Evaluation Framework
class LLMEvaluator:
def __init__(self, model):
self.model = model
self.metrics = {}
def evaluate_generation_quality(self, test_set):
"""Quantitative metrics for text generation."""
results = {
"perplexity": [],
"bleu": [],
"rouge_l": [],
"bertscore": []
}
for example in test_set:
output = self.model.generate(example["input"])
# Perplexity
results["perplexity"].append(
self.calculate_perplexity(example["input"], output)
)
# BLEU for translation-like tasks
results["bleu"].append(
sentence_bleu([example["reference"]], output)
)
# ROUGE-L for summarization
results["rouge_l"].append(
rouge_scorer.score(example["reference"], output)["rougeL"].fmeasure
)
return {k: np.mean(v) for k, v in results.items()}
def evaluate_with_llm_judge(self, test_set, judge_model):
"""LLM-as-a-judge evaluation."""
judge_prompt = """Rate the following response on a scale of 1-5:
Question: {question}
Response: {response}
Criteria:
- Accuracy: Is the information correct?
- Completeness: Does it fully answer the question?
- Clarity: Is it well-written and clear?
Provide scores as JSON: {{"accuracy": X, "completeness": X, "clarity": X}}
"""
scores = []
for example in test_set:
response = self.model.generate(example["input"])
judgment = judge_model.generate(
judge_prompt.format(question=example["input"], response=response)
)
scores.append(json.loads(judgment))
return aggregate_scores(scores)
Key Benchmarks to Consider
| Benchmark | Purpose | When to Use |
|---|---|---|
| MMLU | General knowledge | Broad capability assessment |
| HumanEval | Code generation | Coding applications |
| MTEB | Embedding quality | RAG systems |
| IFEval | Instruction following | Chat/assistant models |
| TruthfulQA | Factual accuracy | High-stakes applications |
Part 5: Managing Hallucinations
Root Causes
- Self-Delusion: Models can’t distinguish their generations from input
- Knowledge Mismatch: Fine-tuning data conflicts with pre-training knowledge
- Structural Limitations: Probabilistic generation without grounding
Technical Mitigations
class HallucinationMitigation:
def __init__(self, model, retriever, fact_checker):
self.model = model
self.retriever = retriever
self.fact_checker = fact_checker
def generate_with_grounding(self, query):
# Retrieve relevant facts first
facts = self.retriever.retrieve(query, k=5)
fact_context = "\n".join([f"- {fact}" for fact in facts])
prompt = f"""Based ONLY on these facts:
{fact_context}
Answer the question. If the facts don't contain the answer, say "I don't have enough information."
Question: {query}
Answer:"""
response = self.model.generate(prompt)
return response
def verify_response(self, query, response):
"""Post-generation fact checking."""
# Extract claims from response
claims = self.extract_claims(response)
verified_claims = []
for claim in claims:
# Check against retrieval
evidence = self.retriever.retrieve(claim, k=3)
is_supported = self.fact_checker.verify(claim, evidence)
verified_claims.append({
"claim": claim,
"supported": is_supported,
"evidence": evidence
})
return verified_claims
def self_consistency_check(self, query, n_samples=5):
"""Generate multiple responses and check consistency."""
responses = [self.model.generate(query, temperature=0.7)
for _ in range(n_samples)]
# Extract key facts from each response
fact_sets = [self.extract_facts(r) for r in responses]
# Find consistent facts (appear in majority)
all_facts = {}
for facts in fact_sets:
for fact in facts:
all_facts[fact] = all_facts.get(fact, 0) + 1
consistent = [f for f, count in all_facts.items()
if count >= n_samples // 2 + 1]
return consistent
Detection Methods
def attention_based_hallucination_detection(model, prompt, response):
"""
Lookback ratio: Do generated tokens attend to input or just previous generations?
Low lookback ratio suggests hallucination.
"""
full_sequence = prompt + response
_, attention_weights = model(full_sequence, output_attentions=True)
prompt_length = len(tokenize(prompt))
response_length = len(tokenize(response))
# For each response token, calculate attention to prompt vs. response
lookback_ratios = []
for i in range(response_length):
response_position = prompt_length + i
attn = attention_weights[-1][0, :, response_position, :] # Last layer
prompt_attention = attn[:, :prompt_length].sum()
total_attention = attn[:, :response_position].sum()
lookback_ratios.append(prompt_attention / total_attention)
return np.mean(lookback_ratios) # Low = likely hallucination
Part 6: Advanced Architecture Considerations
KV Cache Optimization
def calculate_kv_cache_size(
num_layers,
hidden_size,
num_heads,
sequence_length,
batch_size,
precision_bytes=2 # fp16
):
"""
KV cache grows linearly with sequence length.
This is often the memory bottleneck for long contexts.
"""
head_dim = hidden_size // num_heads
# K and V for each layer, each head
kv_per_token = 2 * num_layers * num_heads * head_dim * precision_bytes
total_bytes = batch_size * sequence_length * kv_per_token
return total_bytes / (1024 ** 3) # GB
# Example: LLaMA 70B with 32K context
# calculate_kv_cache_size(80, 8192, 64, 32768, 1) → ~40GB just for KV cache!
Reasoning Enhancement
class ChainOfThoughtGenerator:
"""Implement reasoning patterns."""
def __init__(self, model):
self.model = model
def generate_with_cot(self, question):
prompt = f"""{question}
Let's think through this step by step:
1."""
return self.model.generate(prompt)
def self_consistency(self, question, n_paths=5):
"""Generate multiple reasoning paths, vote on answer."""
answers = []
for _ in range(n_paths):
response = self.generate_with_cot(question)
answer = self.extract_final_answer(response)
answers.append(answer)
# Majority vote
from collections import Counter
return Counter(answers).most_common(1)[0][0]
def tree_of_thought(self, question, breadth=3, depth=3):
"""Explore multiple reasoning branches."""
def expand_node(state, depth_remaining):
if depth_remaining == 0:
return self.evaluate_state(state)
# Generate multiple next steps
next_steps = self.generate_next_steps(state, n=breadth)
# Recursively explore each branch
best_score = float('-inf')
best_path = None
for step in next_steps:
new_state = state + "\n" + step
score, path = expand_node(new_state, depth_remaining - 1)
if score > best_score:
best_score = score
best_path = path
return best_score, best_path
initial_state = f"Question: {question}\nReasoning:"
_, best_reasoning = expand_node(initial_state, depth)
return best_reasoning
Conclusion: The Integrated Approach
The most effective LLM systems recognize that training, deployment, and evaluation aren’t separate phases—they’re interconnected:
- Training choices directly impact deployment options (model size, quantization compatibility)
- Deployment constraints should inform training decisions (target hardware, latency requirements)
- Evaluation insights guide both training data curation and architecture decisions
Recommended Workflow
- Start with evaluation: Define success metrics before training
- Prototype with prompting: Validate the approach before fine-tuning
- Fine-tune incrementally: Start with LoRA, move to full fine-tuning if needed
- Deploy with monitoring: Track hallucination rates, latency, user feedback
- Iterate based on data: Use production insights to improve training data
The most impactful improvements often come from recognizing system-wide relationships rather than optimizing components in isolation.
What challenges have you faced deploying LLMs in production? I’d love to hear about your experiences and discuss solutions.