Tối ưu chi phí Cloud cho dự án AI 2026: FinOps + LLM Cost Optimization thực chiến
Cái hóa đơn $47k và bài học đau nhất
Tháng 3 năm nay, team mình nhận bill AWS. $47,231.
Không phải startup nào đó mình nghe kể lại — là team mình. Pipeline RAG + chatbot, vừa scale lên production tháng trước. Mọi người ăn mừng vì traffic tăng, rồi bill về thì im lặng.
Lúc đó mình không biết là tệ cỡ nào. Cứ nghĩ "ừ thì scale mà, phải tốn thôi". Cho đến khi CFO hỏi "cost per active user là bao nhiêu" — mình tính ra $94/tháng. Với một app freemium.
Bài viết này là những gì mình học được sau 6 tuần ngồi tối ưu từng đồng một. Không phải lý thuyết — đây là cái mình đã làm, cái đã work, cái fail, và tại sao.
Trước khi nói kỹ thuật: tại sao AI tốn khác hẳn SaaS thường
Hồi làm web app thuần, chi phí cloud khá predictable. EC2 chạy 24/7, RDS, S3 — tất cả đều tỉ lệ thuận với user. Dễ estimate, dễ alert.
AI app thì không như vậy.
Vấn đề đầu tiên: một request không bằng một request. User hỏi "shop mở mấy giờ" vs user upload PDF 40 trang rồi hỏi "tóm tắt cho tao" — hai cái này token count chênh nhau 200 lần. AWS Cost Explorer không nói cho bạn điều đó.
Vấn đề thứ hai: chi phí ẩn rất nhiều. Hồi mình audit lần đầu mới phát hiện embedding pipeline đang re-embed toàn bộ document base mỗi ngày vì... ai đó set cron job nhầm. $1,200 đốt trong im lặng suốt 3 tuần.
Vấn đề thứ ba: tối ưu sai chỗ. Mình đã mất cả tuần rightsizing EC2 instance — tiết kiệm được khoảng $180. Trong khi đó cái prompt system đang gửi 8,000 token context cho mỗi câu hỏi đơn giản. Sửa cái đó trong 2 tiếng, tiết kiệm $4,000/tháng.
FinOps cho AI: framework thực tế
Mình không phải fan của framework nặng. Nhưng cái FinOps 3-giai-đoạn (Inform → Optimize → Operate) thực ra đúng, và quan trọng là phải theo đúng thứ tự đó.
Inform trước. Nghe có vẻ hiển nhiên nhưng hầu hết team bỏ qua bước này. Mình đã từng nhảy thẳng vào "optimize" mà không biết mình đang optimize cái gì. Kết quả là tốn một tuần implement semantic cache cho một feature mà hóa ra chỉ chiếm 3% tổng chi phí.
Visualize cụ thể: cost của bạn đang phân bổ thế nào? Cái này cần làm trước mọi thứ.
Typical breakdown (sau khi mình đo thực tế):
- LLM API calls: 48%
- GPU inference (self-host): 22%
- Vector DB: 14%
- Embedding pipeline: 9%
- Data transfer + misc: 7%
Con số này của mình. Team bạn sẽ khác — chính vì vậy phải đo trước.
Setup visibility — đây là việc đầu tiên cần làm
Trước khi tối ưu bất cứ gì, bạn cần biết mình đang tiêu gì.
Mình dùng LiteLLM làm proxy trước tất cả LLM calls. Nó free, open-source, và track cost tự động theo model. Setup mất khoảng 2 tiếng.
# litellm_config.yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: gpt-4o-mini
litellm_params:
model: openai/gpt-4o-mini
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-haiku
litellm_params:
model: anthropic/claude-haiku-4-5-20251001
api_key: os.environ/ANTHROPIC_API_KEY
general_settings:
master_key: "sk-1234"
# Track cost theo metadata
litellm_settings:
success_callback: ["langfuse"]
failure_callback: ["langfuse"]
Và code gọi:
import litellm
response = litellm.completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
metadata={
"project": "chatbot",
"feature": "faq_search", # Quan trọng — tag theo feature, không phải theo service
"user_tier": "free"
}
)
cost = litellm.completion_cost(completion_response=response)
Cái feature tag này quan trọng hơn mình nghĩ. Sau 1 tuần collect data, mình thấy feature "summarize document" chiếm 61% tổng cost nhưng chỉ có 8% request. Đó là chỗ cần tập trung, không phải mấy feature nhỏ lẻ khác.
Kỹ thuật 1: Model Routing — thứ tiết kiệm nhiều nhất
Đây là kỹ thuật mang lại ROI cao nhất trong trường hợp của mình, và cũng là cái mình đáng lẽ phải làm từ đầu.
Bài học đau: ban đầu mình route tất cả qua GPT-4o vì "an toàn". Kết quả là dùng F1 car để đi chợ.
Thực tế là khoảng 60-70% query của user rất đơn giản: hỏi giờ mở cửa, hỏi chính sách hoàn tiền, hỏi các câu FAQ cơ bản. Không cần GPT-4o cho việc đó.
Mình implement router khá đơn giản — không dùng thư viện phức tạp, chỉ dùng embedding similarity:
import numpy as np
from openai import OpenAI
from functools import lru_cache
client = OpenAI()
# Những câu ví dụ cho từng "độ phức tạp"
SIMPLE_EXAMPLES = [
"shop mở mấy giờ",
"giá sản phẩm X là bao nhiêu",
"làm sao đổi mật khẩu",
"tôi muốn hủy đơn hàng",
"shipping mất mấy ngày",
]
COMPLEX_EXAMPLES = [
"so sánh điểm khác nhau giữa gói Basic và Premium",
"giải thích tại sao đơn hàng của tôi bị delay",
"tóm tắt contract này và highlight những điều khoản quan trọng",
"phân tích feedback của khách hàng trong file này",
]
@lru_cache(maxsize=1)
def get_example_embeddings():
simple_embs = client.embeddings.create(
input=SIMPLE_EXAMPLES, model="text-embedding-3-small"
).data
complex_embs = client.embeddings.create(
input=COMPLEX_EXAMPLES, model="text-embedding-3-small"
).data
return (
np.mean([e.embedding for e in simple_embs], axis=0),
np.mean([e.embedding for e in complex_embs], axis=0),
)
def route_query(query: str) -> str:
simple_centroid, complex_centroid = get_example_embeddings()
query_emb = client.embeddings.create(
input=[query], model="text-embedding-3-small"
).data[0].embedding
query_emb = np.array(query_emb)
sim_simple = np.dot(query_emb, simple_centroid) / (
np.linalg.norm(query_emb) * np.linalg.norm(simple_centroid)
)
sim_complex = np.dot(query_emb, complex_centroid) / (
np.linalg.norm(query_emb) * np.linalg.norm(complex_centroid)
)
# Threshold này mình tune bằng tay trên 200 sample queries thật
# Lần đầu set 0.5 thì route nhầm nhiều, cuối cùng landing ở 0.15 delta
if sim_simple - sim_complex > 0.15:
return "gpt-4o-mini" # ~$0.15/1M input
return "gpt-4o" # ~$5/1M input
Một điều mình học được: đừng tin số liệu marketing của router libraries. RouteLLM quảng cáo giảm 85% cost, nhưng trên data thực của mình chỉ được khoảng 58%. Vẫn tốt, nhưng đừng kỳ vọng quá.
Kết quả sau 2 tuần: 68% queries route sang mini, tổng bill LLM giảm từ $22k xuống còn $9.3k. Con số này không "hoàn hảo" — một số query bị route sai, chất lượng output hơi giảm ở vài edge case, team CS feedback lại mình fix thêm vài lần.
Kỹ thuật 2: Semantic Caching — không phải lúc nào cũng đơn giản
Cái này mình implement sớm, và cũng là cái mình fail lần đầu.
Lần 1 fail: Cache hit rate chỉ đạt 11% sau 1 tuần. Lý do: user hỏi bằng tiếng Việt tự nhiên, rất noisy. "ship hàng mất mấy ngày" vs "giao hàng bao lâu" vs "đặt xong bao giờ nhận được" — về nghĩa giống nhau 90% nhưng cosine similarity chỉ ~0.71, thấp hơn threshold 0.92 của mình.
Lần 2 sau khi fix: Thêm bước normalize query trước khi embed — lowercase, bỏ dấu câu thừa, chuẩn hóa số, bỏ stopwords tiếng Việt. Hit rate lên 38%. Vẫn không ấn tượng lắm.
Lần 3: Nhận ra vấn đề là threshold quá cao. Hạ xuống 0.88, hit rate lên 47%, nhưng bắt đầu có false positive — trả về cached answer sai ngữ cảnh. Cuối cùng land ở 0.91 với normalize, hit rate thực tế ~41%.
41% không phải con số "45-60%" như trong các blog khác hay quote. Nhưng với traffic của mình, nó tiết kiệm ~$3,400/tháng, nên vẫn worth it.
import re
import unicodedata
from sentence_transformers import SentenceTransformer
import numpy as np
import redis
import hashlib
import json
# Vietnamese stopwords (subset)
VI_STOPWORDS = {"tôi", "bạn", "mình", "ạ", "nhé", "à", "ơi", "thì", "mà", "là", "có", "được", "cho"}
def normalize_query(text: str) -> str:
"""Normalize tiếng Việt trước khi embed — bước này quan trọng hơn mình nghĩ ban đầu."""
text = text.lower().strip()
# Bỏ dấu câu thừa nhưng giữ nội dung
text = re.sub(r'[^\w\s]', ' ', text)
text = re.sub(r'\s+', ' ', text)
# Bỏ stopwords
words = [w for w in text.split() if w not in VI_STOPWORDS]
return ' '.join(words)
class SemanticCache:
def __init__(self, redis_client: redis.Redis, threshold: float = 0.91):
self.redis = redis_client
# BAAI/bge-m3 support tiếng Việt tốt hơn all-MiniLM-L6-v2
self.model = SentenceTransformer('BAAI/bge-m3')
self.threshold = threshold
def get(self, query: str):
normalized = normalize_query(query)
query_emb = self.model.encode([normalized])[0]
# Lưu ý: redis.keys() scan không scale khi cache lớn.
# Production nên dùng Redis Vector Search hoặc Qdrant riêng.
# Cái này đủ dùng cho ~10k cached entries.
keys = self.redis.keys("scache:*")
best_score = 0
best_result = None
for key in keys:
data = self.redis.hgetall(key)
if not data:
continue
cached_emb = np.frombuffer(data[b'emb'], dtype=np.float32)
score = float(np.dot(query_emb, cached_emb) / (
np.linalg.norm(query_emb) * np.linalg.norm(cached_emb) + 1e-10
))
if score > best_score:
best_score = score
best_result = data
if best_score >= self.threshold and best_result:
return True, best_result[b'response'].decode('utf-8'), best_score
return False, None, best_score
def set(self, query: str, response: str, ttl: int = 3600):
normalized = normalize_query(query)
emb = self.model.encode([normalized])[0].astype(np.float32)
key = f"scache:{hashlib.md5(normalized.encode()).hexdigest()}"
self.redis.hset(key, mapping={
'query': query,
'response': response,
'emb': emb.tobytes()
})
self.redis.expire(key, ttl)
Kỹ thuật 3: Prompt Caching — dễ nhất, nhiều người bỏ qua nhất
Nếu bạn dùng Claude, đây là cái nên làm trong vòng 1 buổi sáng.
Ý tưởng: phần system prompt và context document bạn gửi đi giống nhau cho nhiều request — Claude sẽ cache phần đó lại và charge rẻ hơn nhiều.
import anthropic
client = anthropic.Anthropic()
# Giả sử system prompt + product catalog của bạn = 6,000 tokens
SYSTEM_BASE = "Bạn là trợ lý hỗ trợ khách hàng cho Cửa hàng X..."
PRODUCT_CATALOG = "..." # 5,000 tokens nội dung
def chat_with_caching(user_message: str) -> str:
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=512,
system=[
{
"type": "text",
"text": SYSTEM_BASE,
},
{
"type": "text",
"text": PRODUCT_CATALOG,
"cache_control": {"type": "ephemeral"} # Cache này
}
],
messages=[{"role": "user", "content": user_message}]
)
return response.content[0].text
Với mình, system prompt + context là khoảng 7,200 tokens. Cache hit (sau request đầu tiên) chỉ charge 10% giá bình thường cho phần đó. Cache miss thì charge thêm 25%. Nếu >50% request là cache hit (thường là vậy vì context ít thay đổi), bạn tiết kiệm được ~40-50% input token cost.
Kỹ thuật 4: Ép output ngắn lại
Cái này mình học được từ một incident khá buồn cười. Có một pipeline phân tích sentiment, mỗi call trả về đoạn giải thích dài 400-600 token. Trong khi downstream chỉ cần đọc field sentiment: positive/negative/neutral.
# Cũ — model tự do "giải thích cảm xúc"
old_prompt = f"Phân tích cảm xúc của review này:\n{review_text}"
# Output: "Review này thể hiện cảm xúc tích cực vì người dùng sử dụng
# các từ như 'tuyệt vời', 'hài lòng'... [400 tokens]"
# Mới — ép format JSON ngắn
new_prompt = f"""Phân tích sentiment. CHỈ trả về JSON:
{{"sentiment": "positive|negative|neutral", "confidence": 0.0-1.0}}
Review: {review_text}"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": new_prompt}],
response_format={"type": "json_object"},
max_tokens=30 # sentiment JSON chỉ cần ~20 tokens
)
Pipeline này xử lý 50,000 reviews/tháng. Thay đổi prompt + max_tokens từ default 1024 xuống 30 — tiết kiệm 97% output tokens cho pipeline đó. Tổng saving: ~$1,100/tháng chỉ từ một thay đổi nhỏ.
Kỹ thuật 5: Batch API — 50% cheaper, ít người dùng
OpenAI có Batch API, submit job rồi lấy kết quả trong 24 giờ, giá chỉ bằng một nửa so với realtime. Nếu có workload không cần realtime (analysis, labeling, report generation ban đêm...) thì đây là win dễ nhất.
import json
from openai import OpenAI
from pathlib import Path
client = OpenAI()
def run_batch_analysis(items: list[dict]) -> str:
"""Submit batch job và trả về batch_id để poll sau."""
requests = []
for i, item in enumerate(items):
requests.append({
"custom_id": f"item-{i}-{item['id']}",
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "Phân tích review và trả về JSON: {sentiment, score, key_issues}"
},
{
"role": "user",
"content": item['review_text']
}
],
"max_tokens": 100,
"response_format": {"type": "json_object"}
}
})
# Ghi file JSONL
batch_file_path = Path("/tmp/batch_input.jsonl")
with open(batch_file_path, "w", encoding="utf-8") as f:
for req in requests:
f.write(json.dumps(req, ensure_ascii=False) + "\n")
# Upload và submit
with open(batch_file_path, "rb") as f:
batch_file = client.files.create(file=f, purpose="batch")
batch = client.batches.create(
input_file_id=batch_file.id,
endpoint="/v1/chat/completions",
completion_window="24h"
)
print(f"Submitted batch: {batch.id}")
return batch.id
def check_and_collect(batch_id: str) -> list[dict] | None:
"""Gọi function này định kỳ để check status."""
batch = client.batches.retrieve(batch_id)
if batch.status == "completed":
output_content = client.files.content(batch.output_file_id).text
results = []
for line in output_content.strip().split("\n"):
item = json.loads(line)
if item["response"]["status_code"] == 200:
content = item["response"]["body"]["choices"][0]["message"]["content"]
results.append({
"id": item["custom_id"],
"result": json.loads(content)
})
return results
print(f"Status: {batch.status} | Completed: {batch.request_counts.completed}/{batch.request_counts.total}")
return None
Mình dùng Batch API cho tất cả analytics chạy ban đêm — report generation, sentiment analysis hàng loạt, tagging sản phẩm. Chuyển từ realtime sang batch, tiết kiệm đúng 50% như họ quảng cáo. Thực ra hơn vì còn bỏ được rate limit handling code phức tạp.
Kỹ thuật 6: RAG — chunking và embedding model
Phần này nhiều người tối ưu sai cách.
Sai lầm phổ biến 1: Dùng text-embedding-3-large ($0.13/1M tokens) khi text-embedding-3-small ($0.02/1M tokens) cho kết quả retrieval gần tương đương với tiếng Anh, và với tiếng Việt thì BAAI/bge-m3 self-host còn tốt hơn cả hai.
Sai lầm phổ biến 2: Chunk size quá lớn. Mình từng dùng chunk 2,000 tokens và retrieve top-5 — tức là đang nhét 10,000 tokens context vào mỗi query. Giảm xuống chunk 400-500 tokens với top-3, chất lượng answer không đổi nhiều (thực ra hơi tốt hơn vì context chính xác hơn), cost input giảm 70%.
Sai lầm phổ biến 3: Re-embed khi không cần thiết. Tài liệu không đổi thì không cần embed lại. Mình đã burn $1,200 vì cái cron job nhầm như đã kể ở trên.
from langchain.text_splitter import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer
import hashlib
import json
import redis
# Embedding model tốt cho tiếng Việt, self-host, không tốn tiền per-call
embed_model = SentenceTransformer('BAAI/bge-m3')
splitter = RecursiveCharacterTextSplitter(
chunk_size=450,
chunk_overlap=50, # Overlap nhỏ thôi — không cần nhiều
separators=["\n\n", "\n", ". ", " "]
)
def embed_document_with_cache(doc_id: str, content: str, redis_client: redis.Redis):
"""Chỉ embed lại khi content thực sự thay đổi."""
content_hash = hashlib.md5(content.encode()).hexdigest()
cache_key = f"doc_hash:{doc_id}"
stored_hash = redis_client.get(cache_key)
if stored_hash and stored_hash.decode() == content_hash:
print(f"Skip embedding doc {doc_id} — content unchanged")
return False # Không cần embed lại
chunks = splitter.split_text(content)
embeddings = embed_model.encode(chunks, batch_size=32, show_progress_bar=False)
# Lưu embeddings... (vào vector DB)
# ...
redis_client.setex(cache_key, 86400 * 7, content_hash) # Cache hash 7 ngày
print(f"Embedded {len(chunks)} chunks for doc {doc_id}")
return True
Kỹ thuật 7: GPU — đừng thuê to khi không cần
Cái này applicable nếu bạn self-host model.
Mình từng chạy Llama 3.1 8B trên A100 40GB. Lý do: "cho chắc, đủ room". Nhưng model 8B chỉ cần ~16GB VRAM ở FP16. A100 idle 60% thời gian.
Chuyển sang L4 24GB ($0.72/hr trên AWS) với quantization 4-bit — model 8B chỉ dùng ~5GB VRAM, inference speed tương đương. Saving: từ $2.52/hr (p3.2xlarge với V100) xuống $0.72/hr, gần 71%.
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
# 4-bit quantization: giảm VRAM ~75%, tốc độ giảm khoảng 10-15% so với FP16
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4"
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
quantization_config=bnb_config,
device_map="auto",
torch_dtype=torch.float16
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
# Memory footprint:
# FP16: ~16GB → cần A100 40GB
# 4-bit: ~4.5GB → chạy thoải mái trên L4 24GB
# 8-bit: ~8GB → vẫn fit L4, quality tốt hơn 4-bit một chút
Scale-to-zero là cần thiết nếu traffic không đều. Dev environment không nên chạy GPU 24/7. Mình dùng KEDA trên Kubernetes để scale deployment về 0 replica khi không có request trong 10 phút — tiết kiệm ~$400/tháng cho dev cluster.
Kỹ thuật 8: Spot Instances cho training
Training job thì dùng Spot, không cần bàn cãi. Với AWS p3/p4 instances, spot thường rẻ hơn on-demand 60-70%.
Chỉ cần handle interruption đúng cách:
import boto3
import signal
import sys
class SpotInterruptionHandler:
"""Handle AWS Spot interruption 2-minute warning."""
def __init__(self, checkpoint_fn):
self.checkpoint_fn = checkpoint_fn
self._setup_handlers()
def _setup_handlers(self):
signal.signal(signal.SIGTERM, self._handle_termination)
def _handle_termination(self, signum, frame):
print("Spot interruption signal received. Saving checkpoint...")
self.checkpoint_fn()
sys.exit(0)
def check_imds(self) -> bool:
"""Poll IMDS endpoint để detect sắp bị terminate."""
import requests
try:
r = requests.get(
"http://169.254.169.254/latest/meta-data/spot/termination-time",
timeout=1
)
return r.status_code == 200
except:
return False
# Trong training loop:
handler = SpotInterruptionHandler(checkpoint_fn=save_checkpoint)
for epoch in range(num_epochs):
for batch in dataloader:
if handler.check_imds():
save_checkpoint()
break
loss = train_step(batch)
# Checkpoint mỗi 500 steps
if global_step % 500 == 0:
save_checkpoint()
Kỹ thuật 9: Unit Economics — đo đúng mới tối ưu được
Đây là mindset shift quan trọng nhất: đừng chỉ nhìn tổng cost, hãy nhìn cost per unit of value.
"Tháng này tốn $9,200" không nói lên điều gì nhiều. Nhưng "$0.018 per active user per day" hoặc "$0.003 per search query với conversion rate 2.4%" thì rất actionable.
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import sqlite3
@dataclass
class RequestMetrics:
request_id: str
feature: str
llm_cost_usd: float
infra_cost_usd: float
latency_ms: float
timestamp: datetime = field(default_factory=datetime.now)
user_id: Optional[str] = None
converted: bool = False # Có dẫn đến action có giá trị không?
revenue_attributed: float = 0.0
class UnitEconomicsDB:
def __init__(self, db_path: str):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self._init_schema()
def _init_schema(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS request_metrics (
request_id TEXT PRIMARY KEY,
feature TEXT,
llm_cost REAL,
infra_cost REAL,
total_cost REAL,
latency_ms REAL,
timestamp TEXT,
user_id TEXT,
converted INTEGER,
revenue REAL
)
""")
self.conn.commit()
def record(self, m: RequestMetrics):
self.conn.execute("""
INSERT OR REPLACE INTO request_metrics VALUES (?,?,?,?,?,?,?,?,?,?)
""", (
m.request_id, m.feature,
m.llm_cost_usd, m.infra_cost_usd,
m.llm_cost_usd + m.infra_cost_usd,
m.latency_ms, m.timestamp.isoformat(),
m.user_id, int(m.converted), m.revenue_attributed
))
self.conn.commit()
def get_feature_economics(self, feature: str, days: int = 30) -> dict:
cur = self.conn.execute("""
SELECT
COUNT(*) as requests,
AVG(total_cost) as avg_cost_per_req,
SUM(total_cost) as total_cost,
AVG(CASE WHEN converted = 1 THEN 1.0 ELSE 0.0 END) as conversion_rate,
SUM(revenue) as total_revenue
FROM request_metrics
WHERE feature = ?
AND timestamp > datetime('now', ? || ' days')
""", (feature, f'-{days}'))
row = cur.fetchone()
if not row or row[0] == 0:
return {}
requests, avg_cost, total_cost, conv_rate, total_rev = row
margin = (total_rev - total_cost) / total_rev if total_rev > 0 else -1
return {
"feature": feature,
"requests": requests,
"avg_cost_per_request": round(avg_cost, 5),
"total_cost": round(total_cost, 2),
"conversion_rate": round(conv_rate * 100, 1),
"total_revenue": round(total_rev, 2),
"gross_margin": round(margin * 100, 1),
"profitable": margin > 0
}
Với data này, mình phát hiện feature "auto-generate product description" có margin âm — mỗi request tốn $0.043 nhưng conversion value chỉ $0.031 trung bình. Tắt feature đó cho free tier, chỉ để paid users dùng — tiết kiệm $2,100/tháng.
So sánh Cloud Provider cho AI (tháng 5/2026)
Bảng giá thay đổi liên tục, mình chỉ capture snapshot hiện tại. Luôn kiểm tra lại trước khi ra quyết định lớn.
LLM API Pricing
| Provider & Model | Input ($/1M tokens) | Output ($/1M tokens) | Điểm mạnh |
|---|---|---|---|
| OpenAI GPT-4o | $5.00 | $15.00 | Best general performance |
| OpenAI GPT-4o-mini | $0.15 | $0.60 | Best value cho task nhỏ |
| OpenAI o1 | $15.00 | $60.00 | Reasoning phức tạp |
| Anthropic Claude Haiku 4.5 | $0.25 | $1.25 | Nhanh nhất, rẻ |
| Anthropic Claude Sonnet 4.5 | $3.00 | $15.00 | Balance tốt |
| Google Gemini 1.5 Flash | $0.075 | $0.30 | Rẻ nhất capable model |
| Google Gemini 1.5 Pro | $3.50 | $10.50 | Long context (1M tokens) |
| AWS Bedrock Nova Pro | $0.80 | $3.20 | AWS-native, không data egress |
Lưu ý: Gemini Flash rẻ đến mức mình đang test replace GPT-4o-mini cho một số pipeline. Chất lượng tiếng Việt ổn nhưng chưa đủ thời gian đánh giá kỹ.
GPU Pricing (On-demand)
| GPU | AWS ($/hr) | GCP ($/hr) | Azure ($/hr) | Phù hợp |
|---|---|---|---|---|
| L4 24GB | $0.72 (g6.xlarge) | $0.62 | — | Inference 7-13B |
| A10G 24GB | $1.006 (g5.xlarge) | $0.90 | $0.85 | Inference medium |
| A100 40GB | ~$3.51 | $2.93 | $3.40 | Training / large inference |
| H100 80GB | $9.89 (p5.xlarge) | $8.01 | $7.20 | Large model training |
Spot instances thường rẻ hơn on-demand 60-70% — luôn nên xem spot pricing trước.
Nhận xét cá nhân
GCP rẻ hơn AWS đáng kể cho GPU, nhưng ecosystem AWS mature hơn nhiều. Nếu team bạn đã quen AWS và stack phức tạp, switching cost không worth it chỉ để tiết kiệm 15-20% GPU.
Azure thú vị nếu bạn dùng Azure OpenAI Service — có SLA tốt hơn, data residency options, nhưng đắt hơn. Suitable cho enterprise hơn là startup.
Case Study: Từ $47k xuống $9.2k
Đây là breakdown thực tế sau 6 tuần tối ưu:
TRƯỚC (tháng 3/2026):
├─ LLM API (100% GPT-4o): $31,200
├─ GPU (A100, 24/7): $10,400
├─ Vector DB (Pinecone P2): $3,500
├─ Embedding (3-large): $1,200 ← cái cron job chết tiệt
└─ Storage + transfer: $931
TỔNG: $47,231
Cost per DAU (500 users): $94.46
THAY ĐỔI ĐÃ LÀM:
1. Model routing (68% → mini)
2. Semantic cache (41% hit rate sau normalize)
3. Prompt caching (system prompt 7,200 tokens)
4. max_tokens strict cho từng pipeline
5. Batch API cho analytics pipeline
6. RAG: chunk 450, top-3, embed bằng bge-m3 self-host
7. GPU: L4 thay A100, scale-to-zero khi idle >10min
8. Vector DB: self-host Qdrant trên spot instance
9. Fix cron job embedding (lý do phát hiện: audit log)
SAU (tháng 5/2026):
├─ LLM API (routed + cached): $6,100
├─ GPU (L4, auto-scale): $1,380
├─ Vector DB (Qdrant self-host): $820
├─ Embedding (self-host bge-m3): $0 (chỉ tốn compute)
└─ Storage + transfer: $900
TỔNG: $9,200
Cost per DAU (620 users, tăng): $14.84
Tiết kiệm: $38,031/tháng, tức -80.5%. DAU tăng thêm 120 người mà cost giảm mạnh — đây mới là mục tiêu thật sự.
Quy trình FinOps hàng ngày cho team nhỏ
Đừng setup cái gì phức tạp quá. Team mình 5 người, FinOps "process" rất lean:
Daily (tự động): LiteLLM + Langfuse send Slack alert nếu cost tăng >20% so với average 7 ngày trước. Chỉ cần 1 rule này đã catch được phần lớn anomaly.
Weekly (30 phút thứ Hai): Review cost breakdown theo feature. Có feature nào trending up không? Có cái gì tốn tiền mà chưa rõ lý do không?
Monthly: Nhìn big picture, quyết định có nên migrate gì không (model mới ra, spot pricing thay đổi...).
Tagging — làm ngay từ đầu: Đây là sai lầm mình trả giá đắt nhất. Tháng đầu không tag gì, sau 2 tháng phải đi audit lại toàn bộ resource — mất 2 ngày.
# Tag convention tối thiểu
REQUIRED_TAGS = {
"team": "ai | data | infra | product",
"project": "chatbot-v2 | analytics | ...",
"env": "dev | staging | prod",
"feature": "search | summarize | classify | ...",
}
# Thiếu tag nào → CI/CD pipeline fail, không deploy được
Kết luận: 5 việc làm ngay nếu chưa có gì
Nếu bạn đang đọc đến đây và chưa làm gì cả, đây là thứ tự mình khuyên:
1. Setup visibility (1 ngày): LiteLLM + Langfuse. Không biết tiền chảy đi đâu thì tối ưu mò.
2. Model routing (2 ngày): Implement router đơn giản. Tune threshold trên 100-200 query thật của bạn, không dùng số của người khác.
3. Prompt caching (vài giờ): Nếu dùng Claude hoặc GPT với system prompt dài, thêm cache_control vào ngay.
4. max_tokens discipline (vài giờ): Review tất cả LLM calls, set max_tokens phù hợp với từng use case. Đừng để default.
5. Fix cron jobs và background jobs (1 ngày): Audit xem có gì đang chạy mà bạn không biết không. Mình cá là có.
Lời khuyên cho team nhỏ: Đừng tự build tooling phức tạp. LiteLLM + Langfuse đủ dùng cho 90% trường hợp. Thời gian build tool = thời gian optimize thật.
Lời khuyên cho team lớn: Unit economics quan trọng hơn total cost. $50k/tháng acceptable nếu mỗi dollar generate $3 revenue. $10k/tháng vẫn là vấn đề nếu margin âm.
- Bài được chỉnh sửa bởi AI, nhưng không dùng AI để hoàn toàn generate
All rights reserved