Ollama: Ajuste de Desempenho
Ajuste de desempenho é trocar de marcha no motor de IA local — da primeira marcha rastejante à quinta voadora.
OLLAMA_NUM_PARALLEL controla o número de requisições paralelas por modelo — definir para 4 permite processar 4 requisições simultaneamente, aumentando o throughput. Para 8GB VRAM, comece testando com 2-4; para 16GB, tente 4-8. Combine com OLLAMA_KEEP_ALIVE=30m para evitar descarregamento e recarregamento frequente do modelo.
📋 Pré-requisitos: Você deve dominar o seguinte primeiro
- Aula 9: Configuração de GPU e CUDA
- Aula 15: Implantação Containerizada com Docker
1. O Que Você Vai Aprender
- Métricas de desempenho-chave: Tokens/s, TTFT, Uso de Memória
- Ajuste de requisições concorrentes: OLLAMA_NUM_PARALLEL e OLLAMA_MAX_LOADED_MODELS
- Gerenciamento de janela de contexto: num_ctx e KV Cache
- Processamento de requisições em lote e agendamento de fila
- Ferramentas de benchmarking e estabelecimento de baseline de desempenho
2. Uma História Real de Uma Empreendedora SaaS
OLLAMA_KEEP_ALIVE define quanto tempo um modelo permanece na memória. Para cenários de alta concorrência, defina para 30m ou mais para evitar atrasos de início a frio causados por descarregamento/recarregamento frequente do modelo (5-30 segundos). Para uso infrequente, defina 5m para liberar VRAM.
num_ctx (tamanho da janela de contexto) afeta diretamente o uso de memória do KV Cache. Um modelo 8B com num_ctx=2048 precisa de cerca de 4GB VRAM, num_ctx=8192 precisa de cerca de 6GB, e num_ctx=32768 precisa de cerca de 12GB. Defina com base nas necessidades reais, não aumente cegamente.
(1) O Problema: Timeouts de Resposta Sob Alta Concorrência
O SupportBot da Alice entrou no ar, e durante horários de pico, 10 requisições concorrentes fizeram o tempo de resposta disparar de 2 segundos para 30 segundos. Clientes reclamaram de "esperar demais," mas o Ollama por padrão processa apenas 1 requisição concorrente.
(2) A Solução: Otimização de Concorrência e Contexto
Ajustando OLLAMA_NUM_PARALLEL e OLLAMA_MAX_LOADED_MODELS, o tempo de resposta recuperou para menos de 5 segundos:
# Enable 4 parallel requests
export OLLAMA_NUM_PARALLEL=4
export OLLAMA_MAX_LOADED_MODELS=2
3. Métricas de Desempenho-Chave
OLLAMA_NUM_PARALLEL não aumenta linearmente o throughput — cada requisição concorrente compartilha VRAM da GPU, e muitas requisições concorrentes podem causar OOM ou uma queda acentuada na velocidade por requisição. Comece com 2 e teste incrementalmente, observando o uso de VRAM e mudanças de latência.
(1) Três Métricas Centrais
| Métrica | Nome Completo | Significado | Valor Alvo |
|---|---|---|---|
| Tokens/s | Tokens por segundo | Velocidade de geração | GPU: 30+, CPU: 5+ |
| TTFT | Tempo até o Primeiro Token | Latência do primeiro token | < 500ms (GPU) |
| Memória | Uso VRAM/RAM | Consumo de recursos | < 90% da capacidade |
(2) Estatísticas de Desempenho do Ollama
Dados de desempenho incluídos no modo --verbose ou respostas não-streaming:
| Campo | Significado |
|---|---|
| total_duration | Tempo total (nanossegundos) |
| load_duration | Tempo de carregamento do modelo |
| prompt_eval_count | Contagem de tokens de entrada |
| prompt_eval_duration | Tempo de processamento da entrada |
| eval_count | Contagem de tokens de saída |
| eval_duration | Tempo de geração da saída |
▶ Exemplo 1: Coleta de Métricas de Desempenho
# Collect performance metrics with verbose output
ollama run --verbose qwen2.5 "Explain AI in 50 words"
# Key output:
# total duration: 2500000000 # 2.5s total
# load duration: 500000000 # 0.5s model load
# prompt eval count: 15 token(s)
# prompt eval duration: 200000000 # 0.2s input processing
# prompt eval count: 15 token(s) # = prompt_eval_speed: 75 tok/s
# eval count: 52 token(s)
# eval duration: 1800000000 # = eval_speed: 28.9 tok/s
Saída:
I'm a helpful AI assistant running locally on your machine...
import ollama
import time
def measure_performance(model: str, prompt: str) -> dict:
start = time.time()
response = ollama.chat(
model=model,
messages=[{"role": "user", "content": prompt}],
stream=False
)
total_time = time.time() - start
content = response["message"]["content"]
token_count = len(content) // 4 # Rough estimate
return {
"model": model,
"total_time_s": round(total_time, 2),
"estimated_tokens": token_count,
"estimated_tok_per_s": round(token_count / total_time, 1) if total_time > 0 else 0
}
result = measure_performance("qwen2.5", "Explain machine learning in 100 words")
print(result)
4. Ajuste de Requisições Concorrentes
OLLAMA_NUM_PARALLEL não aumenta linearmente o throughput; cada requisição concorrente compartilha VRAM da GPU, e muitas requisições concorrentes causam uma queda acentuada na velocidade por requisição ou até estouro de memória. Comece com 2 e teste incrementalmente, observando o uso de VRAM e mudanças de latência.
(1) Variáveis de Ambiente Relacionadas à Concorrência
| Variável | Padrão | Descrição | Impacto |
|---|---|---|---|
| OLLAMA_NUM_PARALLEL | 1 | Requisições paralelas por modelo | ↑Throughput ↓Velocidade por requisição |
| OLLAMA_MAX_LOADED_MODELS | 1 | Máximo de modelos carregados simultaneamente | ↑Concorrência multi-modelo ↑VRAM |
| OLLAMA_KEEP_ALIVE | 5m | Tempo de residência do modelo na memória | ↑Evita recarregamento ↓Recuperação de memória |
(2) Decisão de Configuração de Concorrência
flowchart LR
A[Requisições Concorrentes?] --> B{QPS de Pico?}
B -->|1-2| C[Padrão: NUM_PARALLEL=1]
B -->|3-5| D[NUM_PARALLEL=4<br/>8GB VRAM mínimo]
B -->|6-10| E[NUM_PARALLEL=8<br/>16GB+ VRAM]
B -->|10+| F[Multi-nó<br/>Load Balancer]
| Configuração | Requisito VRAM | Throughput | Latência por Requisição |
|---|---|---|---|
| NUM_PARALLEL=1 | Mais baixo | 1 req/s | Mais curta |
| NUM_PARALLEL=4 | Médio | 3-4 req/s | Leve aumento |
| NUM_PARALLEL=8 | Alto | 6-8 req/s | Aumento notável |
▶ Exemplo 2: Configuração e Teste de Concorrência
# Configure parallel processing
sudo systemctl edit ollama
# Add:
[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_MAX_LOADED_MODELS=2"
Environment="OLLAMA_KEEP_ALIVE=30m"
sudo systemctl daemon-reload
sudo systemctl restart ollama
Saída:
# Ollama command executed successfully
import ollama
import asyncio
import time
async def concurrent_test(num_requests: int):
"""Test concurrent request handling."""
client = ollama.AsyncClient()
start = time.time()
async def single_request(i: int):
req_start = time.time()
response = await client.chat(
model="qwen2.5",
messages=[{"role": "user", "content": f"Say hello {i}"}],
stream=False,
options={"temperature": 0.1}
)
return time.time() - req_start
tasks = [single_request(i) for i in range(num_requests)]
latencies = await asyncio.gather(*tasks)
total = time.time() - start
print(f"Concurrent: {num_requests} requests")
print(f"Total time: {total:.2f}s")
print(f"Avg latency: {sum(latencies)/len(latencies):.2f}s")
print(f"Throughput: {num_requests/total:.1f} req/s")
asyncio.run(concurrent_test(4))
5. Gerenciamento de Janela de Contexto
(1) Impacto do num_ctx na Memória
| num_ctx | KV Cache (8B Q4_M) | VRAM Total Necessária | Caso de Uso |
|---|---|---|---|
| 2048 | ~1 GB | ~6 GB | Conversas curtas (padrão) |
| 4096 | ~2 GB | ~7 GB | Conversas médias |
| 8192 | ~4 GB | ~9 GB | Recuperação RAG |
| 32768 | ~16 GB | ~21 GB | Documentos longos |
(2) Estratégias de Otimização do num_ctx
| Estratégia | Método | Efeito |
|---|---|---|
| Definir sob demanda | Chat usa 2048, RAG usa 8192 | Reduz desperdício de memória |
| Janela deslizante | Manter apenas as últimas N rodadas de conversa | Controla comprimento da entrada |
| Compressão por resumo | Periodicamente comprimir conversas iniciais | Economiza espaço de contexto |
| Curadoria RAG | Reduzir top-k de 5 para 3 | Reduz tokens de entrada |
▶ Exemplo 3: Teste de Comparação num_ctx
import ollama
import time
def test_context_sizes(model: str, prompt: str, context_sizes: list[int]):
"""Test performance with different context window sizes."""
for ctx in context_sizes:
start = time.time()
response = ollama.chat(
model=model,
messages=[{"role": "user", "content": prompt}],
stream=False,
options={"num_ctx": ctx, "temperature": 0.3}
)
elapsed = time.time() - start
tokens = len(response["message"]["content"]) // 4
speed = tokens / elapsed if elapsed > 0 else 0
print(f"num_ctx={ctx}: {elapsed:.2f}s, ~{speed:.1f} tok/s")
test_context_sizes("qwen2.5", "Explain AI briefly", [2048, 4096, 8192])
Saída:
# Function defined successfully
6. Processamento em Lote e Benchmarking
(1) Estratégias de Processamento em Lote
| Estratégia | Descrição | Caso de Uso |
|---|---|---|
| Lote serial | Processar requisições uma a uma | Simples e confiável |
| Lote paralelo | Concorrência asyncio | Alto throughput |
| Agendamento de fila | FastAPI + fila | Ambiente de produção |
▶ Exemplo 4: Script de Benchmark em Lote
import ollama
import time
import asyncio
import json
from datetime import datetime
class BenchmarkRunner:
def __init__(self, model: str = "qwen2.5"):
self.model = model
self.results = []
def warmup(self, runs: int = 2):
for _ in range(runs):
ollama.chat(model=self.model,
messages=[{"role": "user", "content": "warmup"}],
stream=False)
def single_benchmark(self, prompt: str) -> dict:
start = time.time()
response = ollama.chat(
model=self.model,
messages=[{"role": "user", "content": prompt}],
stream=False,
options={"temperature": 0.3}
)
elapsed = time.time() - start
content = response["message"]["content"]
return {
"prompt_length": len(prompt),
"response_length": len(content),
"estimated_tokens": len(content) // 4,
"total_time_s": round(elapsed, 2),
"tok_per_s": round(len(content) / 4 / elapsed, 1) if elapsed > 0 else 0
}
async def concurrent_benchmark(self, prompt: str, concurrency: int) -> dict:
client = ollama.AsyncClient()
start = time.time()
tasks = [
client.chat(model=self.model,
messages=[{"role": "user", "content": prompt}],
stream=False, options={"temperature": 0.3})
for _ in range(concurrency)
]
await asyncio.gather(*tasks)
total = time.time() - start
return {
"concurrency": concurrency,
"total_time_s": round(total, 2),
"throughput_rps": round(concurrency / total, 1)
}
def run_full_benchmark(self):
self.warmup()
prompts = [
"Hello",
"Explain AI in 3 sentences",
"Write a product description for wireless headphones"
]
print("=== Single Request Benchmark ===")
for p in prompts:
result = self.single_benchmark(p)
print(f" {result['estimated_tokens']}tok, {result['tok_per_s']}tok/s, {result['total_time_s']}s")
print("\n=== Concurrent Benchmark ===")
for c in [1, 2, 4]:
result = asyncio.run(self.concurrent_benchmark("Hello", c))
print(f" Concurrency {c}: {result['throughput_rps']} req/s")
# Run
runner = BenchmarkRunner("qwen2.5")
runner.run_full_benchmark()
Saída:
=== Single Request Benchmark ===
=== Concurrent Benchmark ===
▶ Exemplo 5: Estabelecimento de Baseline de Desempenho
#!/bin/bash
# Establish performance baseline
MODEL="qwen2.5"
DATE=$(date +%Y%m%d)
REPORT="baseline_${DATE}.txt"
echo "=== Performance Baseline ===" > "$REPORT"
echo "Date: $(date)" >> "$REPORT"
echo "Model: $MODEL" >> "$REPORT"
echo "GPU: $(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || echo 'CPU')" >> "$REPORT"
echo "" >> "$REPORT"
# Single request benchmark
echo "## Single Request ##" >> "$REPORT"
for i in {1..5}; do
start=$(date +%s%N)
curl -s http://localhost:11434/api/chat -d "{
\"model\": \"$MODEL\",
\"messages\": [{\"role\": \"user\", \"content\": \"Hello\"}],
\"stream\": false
}" > /dev/null
end=$(date +%s%N)
elapsed=$(( (end - start) / 1000000 ))
echo " Run $i: ${elapsed}ms" >> "$REPORT"
done
echo "" >> "$REPORT"
echo "## GPU Utilization ##" >> "$REPORT"
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv >> "$REPORT" 2>/dev/null || echo "CPU mode" >> "$REPORT"
echo "Baseline saved: $REPORT"
Saída:
{"status":"ok","data":{}}
7. Exemplo Abrangente: Relatório de Ajuste de Desempenho do SupportBot
# ============================================
# Comprehensive: Performance tuning report
# Full benchmark + optimization recommendations
# ============================================
import ollama
import time
import asyncio
import json
class PerformanceTuner:
def __init__(self, model: str = "qwen2.5"):
self.model = model
def run_diagnostics(self) -> dict:
"""Run full performance diagnostics."""
# Warm up
ollama.chat(model=self.model,
messages=[{"role": "user", "content": "warmup"}],
stream=False)
# Single request test
start = time.time()
resp = ollama.chat(
model=self.model,
messages=[{"role": "user", "content": "Hello"}],
stream=False, options={"temperature": 0.3}
)
single_latency = time.time() - start
# Context size test
ctx_results = {}
for ctx in [2048, 4096, 8192]:
start = time.time()
ollama.chat(
model=self.model,
messages=[{"role": "user", "content": "Hello"}],
stream=False, options={"num_ctx": ctx, "temperature": 0.3}
)
ctx_results[ctx] = round(time.time() - start, 2)
return {
"model": self.model,
"single_latency_s": round(single_latency, 2),
"context_sizes": ctx_results,
"recommendations": self._generate_recommendations(single_latency)
}
def _generate_recommendations(self, latency: float) -> list[str]:
recs = []
if latency > 5:
recs.append("High latency detected. Enable GPU acceleration or use smaller model.")
if latency > 2:
recs.append("Set OLLAMA_NUM_PARALLEL=4 for concurrent requests.")
recs.append("Use num_ctx=2048 for chat, num_ctx=8192 for RAG.")
recs.append("Set OLLAMA_KEEP_ALIVE=30m to avoid model reloading.")
return recs
def generate_report(self) -> str:
diag = self.run_diagnostics()
report = [
f"# Performance Tuning Report",
f"Model: {diag['model']}",
f"Single request latency: {diag['single_latency_s']}s",
f"\n## Context Window Impact:",
]
for ctx, latency in diag["context_sizes"].items():
report.append(f" num_ctx={ctx}: {latency}s")
report.append("\n## Recommendations:")
for r in diag["recommendations"]:
report.append(f" - {r}")
return "\n".join(report)
tuner = PerformanceTuner("qwen2.5")
print(tuner.generate_report())
❓ Perguntas Frequentes
P: Qual deve ser o valor de OLLAMA_NUM_PARALLEL? R: 2-4 para 8GB VRAM, 4-8 para 16GB, 8 para 24GB. Muitas requisições paralelas aumentam a latência por requisição e risco de OOM. Comece testando com 4.
P: E se o TTFT estiver muito alto? R: 1) Garanta que OLLAMA_KEEP_ALIVE é longo o suficiente (evite início a frio); 2) Reduza num_ctx; 3) Use um modelo menor; 4) Garanta que a aceleração GPU está funcionando.
P: Como reduzir o tempo de carregamento do modelo? R: Defina OLLAMA_KEEP_ALIVE=30m ou mais para manter o modelo residente na memória. O primeiro carregamento é inevitável; requisições subsequentes respondem em segundos.
P: E se OOM ocorrer durante requisições concorrentes? R: Reduza OLLAMA_NUM_PARALLEL; use num_ctx menor; reduza OLLAMA_MAX_LOADED_MODELS; ou adicione mais VRAM.
P: E se os resultados do benchmark forem instáveis? R: 1) Aqueça 2-3 vezes para eliminar início a frio; 2) Feche outros programas de GPU; 3) Tire 5+ execuções e faça média; 4) Fixe a semente aleatória.
P: Como monitorar o desempenho em ambiente de produção? R: A Aula 21 detalhará a stack de monitoramento Prometheus + Grafana. Por enquanto, use
--verbosee scripts personalizados para coletar métricas.
📖 Resumo
- Três métricas centrais: Tokens/s (velocidade), TTFT (latência), Memória (recursos)
- OLLAMA_NUM_PARALLEL controla concorrência; 4 é um ponto de partida para 8GB VRAM
- num_ctx afeta linearmente a memória do KV Cache; defina sob demanda: 2048 para chat, 8192 para RAG
- Benchmarks precisam de aquecimento + múltiplas execuções com média; estabeleça baselines de desempenho para rastrear mudanças
- OLLAMA_KEEP_ALIVE evita início a frio; defina 30m para serviços de alta frequência
- Ajuste de produção: teste requisição única → depois concorrente → finalmente contexto longo
📝 Exercícios
- Básico (⭐): Execute o modo
--verbose, registre Tokens/s, TTFT e outras métricas para 3 execuções de Inferência, e estabeleça seu baseline de desempenho. - Intermediário (⭐⭐): Configure OLLAMA_NUM_PARALLEL=4, execute testes concorrentes, e compare mudanças de throughput e latência antes e depois da concorrência.
- Avançado (⭐⭐⭐): Escreva um relatório completo de ajuste de desempenho — incluindo benchmarks de requisição única, testes concorrentes, comparação num_ctx e recomendações de otimização — em formato Markdown.