把算力换到成本时踩过的坑
3000 多美元的 SageMaker 账单,而我们的服务日活还不到 5 万。
用的是 GPT-4 兼容的 7B 模型,部署在 SageMaker,用了 4 个 ml.g5.xlarge 实例。
问题出在哪儿?
先做个成本拆解。用的是 GPT-4 兼容的 7B 模型,部署在 SageMaker,用了 4 个 ml.g5.xlarge 实例。平均每个请求 200 tokens,响应时间 800ms 左右。
# 账单拆解时的关键数据
aws ce get-cost-and-usage \
--time-period Start=2026-06-01,End=2026-06-30 \
--granularity MONTHLY \
--metrics BlendedCost \
--filter '{
"Dimensions": {
"Key": "SERVICE",
"Values": ["Amazon SageMaker"]
}
}' \
--query 'ResultsByTime[0].Total.BlendedCost'
# 输出: {"Amount": "3142.87", "Unit": "USD"}
问题很快就出来了:
- 实例利用率低:监控显示 CPU 长期在 20-30% 之间徘徊
- 冷启动频繁:自动扩缩容策略设得太激进,导致频繁启停实例
- 模型没有量化:直接用 FP16 部署,推理延迟偏高
- 没有请求聚合:每条用户消息都单独跑一次推理,即使可以通过缓存命中
更糟糕的是,团队还在讨论要不要换更大的模型来"提升用户体验"。
第一步:把模型量化和减小
先从最直接的下手。模型用的是 Meta 的 Llama-2-7B-chat-hf,原始 FP16 版本占用约 14GB 显存。我们用 AutoGPTQ 做了 4-bit 量化:
from transformers import AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
# 量化配置
quantize_config = BaseQuantizeConfig(
bits=4, # 4-bit 量化
group_size=128,
damp_percent=0.01,
desc_act=False,
)
# 加载原始模型
model = AutoGPTQForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-chat-hf",
quantize_config=quantize_config,
)
# 准备校准数据
calibration_data = [
"用户: 你好\n助手: 你好!有什么我可以帮助你的吗?",
"用户: 解释一下什么是机器学习\n助手: 机器学习是一种人工智能...",
# ... 准备 100-200 条代表性的对话数据
]
# 执行量化
model.quantize(calibration_data, cache_examples_on_gpu=True)
# 保存量化模型
model.save_quantized("./llama-2-7b-4bit")
量化后模型大小从 14GB 降到约 4.5GB,推理显存占用从 18GB 降到 8GB 左右。这意味我们可以把部署实例从 ml.g5.xlarge(16GB 显存)换成更便宜的 ml.g5.2xlarge,然后在一个实例上跑 2 个模型副本。
但这事儿没那么简单。
量化后第一个坑就来了:某些长文本生成任务偶尔出现"胡言乱语"的现象。用户投诉说"AI 有时会突然说一些完全不通的话"。
查了半天才发现,4-bit 量化在极端长度(超过 2000 tokens)的序列上会出现精度问题。最后做了个妥协:
def get_model_for_request(request_length):
if request_length > 2000:
# 长文本用 FP16 模型
return fp16_model_path
else:
# 短文本用 4-bit 量化模型
return quantized_model_path
这样虽然复杂了点,但确实解决了问题。更重要的是,这个"复杂度"带来了立竿见影的成本下降:月度账单从 3142 美元降到了 1860 美元。
第二步:优化实例调度和扩缩容
接下来处理实例利用率低的问题。之前用的是默认的 SageMaker 自动扩缩容,目标是保持 30% 的 CPU 利用率。问题在于:
# 之前的扩缩容配置(有问题)
ScalingPolicies:
- TargetTrackingScalingPolicyConfiguration:
TargetValue: 30.0 # CPU 利用率目标
PredefinedMetricSpecification:
PredefinedMetricType: SageMakerVariantInvocationsPerInstance
ScaleOutCooldown: 300 # 5 分钟
ScaleInCooldown: 300
这个配置有个致命问题:它只看"每实例调用数",但没考虑"调用耗时"。而我们的调用耗时波动很大(200ms-1500ms),导致扩缩容判断失真。
改用自定义指标后情况好多了:
# 自定义 CloudWatch 指标:计算每个实例的"待处理请求数"
def calculate_backlog_metric(instance_id):
# 获取实例当前处理的请求数
active_requests = get_active_requests(instance_id)
# 获取平均处理时间
avg_process_time = get_avg_process_time(instance_id)
# 估算待处理积压
backlog = active_requests * (avg_process_time / 1000)
return backlog
# 发送到 CloudWatch
cloudwatch.put_metric_data(
Namespace='SageMaker/Custom',
MetricData=[{
'MetricName': 'InstanceBacklog',
'Dimensions': [{'Name': 'InstanceId', 'Value': instance_id}],
'Value': backlog,
'Unit': 'Count'
}]
)
# 优化后的扩缩容配置
ScalingPolicies:
- TargetTrackingScalingPolicyConfiguration:
TargetValue: 5.0 # 目标是每个实例最多积压 5 个请求
CustomizedMetricSpecification:
MetricName: InstanceBacklog
Namespace: SageMaker/Custom
Statistic: Average
ScaleOutCooldown: 60 # 快速扩容
ScaleInCooldown: 600 # 慢速缩容,避免频繁启停
这一步的效果非常直接:实例平均 CPU 利用率从 25% 提升到了 65%,同时实例数量从平均 4 个降到了 2.5 个。
第三步:加个请求缓存层
当时有个很荒谬的现象:我们 30% 的请求其实是"重复的"。用户经常问类似的问题,但我们每次都重新跑一遍推理。
加了个简单的缓存层:
import hashlib
import json
from functools import lru_cache
# 缓存配置
CACHE_TTL = 3600 # 1 小时
CACHE_KEY_PREFIX = "ai_response:"
def generate_cache_key(prompt, model_version, temperature):
"""生成缓存键"""
cache_data = {
"prompt": prompt,
"model": model_version,
"temperature": temperature,
}
return hashlib.sha256(
json.dumps(cache_data, sort_keys=True).encode()
).hexdigest()
async def get_cached_response(cache_key):
"""从 Redis 获取缓存"""
try:
cached = await redis.get(f"{CACHE_KEY_PREFIX}{cache_key}")
if cached:
return json.loads(cached)
except Exception as e:
# 缓存失败不影响主流程
logger.warning(f"Cache get failed: {e}")
return None
async def cache_response(cache_key, response):
"""缓存响应"""
try:
await redis.setex(
f"{CACHE_KEY_PREFIX}{cache_key}",
CACHE_TTL,
json.dumps(response)
)
except Exception as e:
logger.warning(f"Cache set failed: {e}")
async def generate_with_cache(prompt, model_version, temperature=0.7):
"""带缓存的推理"""
cache_key = generate_cache_key(prompt, model_version, temperature)
# 尝试从缓存获取
cached = await get_cached_response(cache_key)
if cached:
logger.info(f"Cache hit: {cache_key[:8]}...")
return cached
# 缓存未命中,执行推理
response = await model.generate(prompt, temperature=temperature)
# 缓存结果
await cache_response(cache_key, response)
return response
这个简单的缓存层带来了 28% 的缓存命中率,直接减少了近三分之一的推理调用。
但这里也有个坑:有些用户反馈"AI 回答太死板了"。后来发现是我们的缓存键太严格,甚至连标点符号的微小差异都会导致缓存未命中。
调整了缓存策略:
def normalize_prompt(prompt):
"""标准化 prompt 以提高缓存命中率"""
# 去除多余空格
prompt = ' '.join(prompt.split())
# 统一标点符号
prompt = prompt.replace(',', ',').replace('。', '.')
# 转小写(仅对英文部分)
import re
prompt = re.sub(
r'[a-zA-Z]+',
lambda m: m.group(0).lower(),
prompt
)
return prompt
调整后缓存命中率提升到了 35%,同时用户投诉也少了。
第四步:混合云部署
做到这一步,账单已经从 3142 美元降到了 980 美元。但团队还是觉得"能不能再低点"。
这时候开始考虑混合云部署:把大部分推理流量放到更便宜的 GPU 实例上,只把关键请求留在 SageMaker。
我们选了 RunPod,因为它的 GPU 实例比 AWS 便宜约 40-50%。但有个问题:RunPod 不支持 SageMaker 那种开箱即用的模型部署,需要自己处理很多运维细节。
最终采用了这样的架构:
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
class MixedInferenceClient:
def __init__(self):
self.sagemaker_endpoint = "https://xxx.execute-api.us-east-1.amazonaws.com/prod"
self.runpod_endpoint = "https://xxx.runpod.io"
self.cache_client = RedisClient()
def is_critical_request(self, prompt):
"""判断是否为关键请求"""
critical_keywords = [
"支付", "退款", "账户", "密码",
"legal", "payment", "account"
]
return any(keyword in prompt.lower() for keyword in critical_keywords)
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
async def call_sagemaker(self, payload):
"""调用 SageMaker 端点"""
async with httpx.AsyncClient() as client:
response = await client.post(
self.sagemaker_endpoint,
json=payload,
timeout=30.0
)
response.raise_for_status()
return response.json()
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
async def call_runpod(self, payload):
"""调用 RunPod 端点"""
async with httpx.AsyncClient() as client:
response = await client.post(
self.runpod_endpoint,
json=payload,
timeout=30.0
)
response.raise_for_status()
return response.json()
async def generate(self, prompt, temperature=0.7):
# 检查缓存
cached = await self.cache_client.get(prompt)
if cached:
return cached
# 判断请求类型
if self.is_critical_request(prompt):
# 关键请求用 SageMaker
response = await self.call_sagemaker({
"prompt": prompt,
"temperature": temperature
})
else:
# 普通请求用 RunPod
response = await self.call_runpod({
"prompt": prompt,
"temperature": temperature,
"max_tokens": 512
})
# 缓存结果
await self.cache_client.set(prompt, response, ttl=3600)
return response
这个混合部署的策略是:约 15% 的关键请求(涉及支付、账户等)走 SageMaker,其余 85% 的普通请求走 RunPod。
效果立竿见影:月度账单从 980 美元降到了 780 美元。但这里也有坑:
- RunPod 稳定性不如 AWS:偶尔会出现实例不可用的情况,需要更好的重试策略
- 延迟稍微增加:跨云调用增加了约 100-200ms 的延迟
- 运维复杂度上升:需要同时管理两套基础设施
针对这些问题,我们做了调整:
# 改进的健康检查和回退策略
async def generate_with_fallback(self, prompt, temperature=0.7):
"""带回退策略的推理"""
# 检查缓存
cached = await self.cache_client.get(prompt)
if cached:
return cached
# 判断请求类型
if self.is_critical_request(prompt):
# 关键请求直接用 SageMaker
response = await self.call_sagemaker({
"prompt": prompt,
"temperature": temperature
})
else:
# 普通请求先尝试 RunPod
try:
response = await asyncio.wait_for(
self.call_runpod({
"prompt": prompt,
"temperature": temperature,
"max_tokens": 512
}),
timeout=5.0 # 5 秒超时
)
except (asyncio.TimeoutError, httpx.HTTPError) as e:
# RunPod 失败,回退到 SageMaker
logger.warning(f"RunPod failed, fallback to SageMaker: {e}")
response = await self.call_sagemaker({
"prompt": prompt,
"temperature": temperature
})
# 缓存结果
await self.cache_client.set(prompt, response, ttl=3600)
return response
这个回退策略保证了即使 RunPod 出问题,核心服务也不会受影响。
最后的账单
经过这几个月的折腾,最终的月度账单大概是这样:
| 项目 | 优化前 | 优化后 | 降幅 |
|---|---|---|---|
| SageMaker 实例 | $2,850 | $380 | 87% |
| RunPod 实例 | $0 | $320 | - |
| CloudWatch/日志 | $180 | $60 | 67% |
| 其他 AWS 服务 | $113 | $20 | 82% |
| 总计 | $3,143 | $780 | 75% |
量化、缓存与混合云三轮优化后,账单结构发生了明显变化——下图按分项对比优化前后的月度费用。

SageMaker 实例费用从 $2,850 降到 $380,RunPod 承接了大部分普通流量,总账单降幅达 75%。
关键指标变化:
- 平均推理延迟:800ms → 650ms(反而提升了)
- 缓存命中率:0% → 35%
- 实例平均 CPU 利用率:25% → 68%
- P99 响应时间:2.3s → 1.8s
经验总结
回顾整个过程,有些经验值得分享:
先监控再优化:很多团队一上来就想"用什么技术降成本",但连钱花在哪儿都不知道。先做详细的成本拆解和监控,找到真正的浪费点。
量化是最直接的下手点:模型量化虽然不是银弹,但确实是成本优化中最直接的手段。4-bit 量化在我们的场景下几乎没有明显的质量损失。
缓存不是万能的:缓存能显著降低推理调用,但要注意缓存失效策略和用户体验的平衡。我们踩过"缓存太严格导致用户体验变差"的坑。
混合云要慎重:混合部署确实能降低成本,但会增加运维复杂度。小团队在考虑前要评估是否有足够的人力维护多套基础设施。
不要过度优化:从 3143 美元降到 780 美元后,再往下优化的边际收益就很低了。与其继续抠那几十美元,不如把精力放在产品功能上。
这次优化的本质不是"技术多牛",而是"账单逼出来的务实选择"。很多时候,成本优化的第一步不是选什么框架或算法,而是先搞清楚钱到底花在哪儿。
最后留个问题:如果你的 AI 服务月度账单突然翻倍,你会先查哪儿?
版权声明: 本文首发于 指尖魔法屋-把算力换到成本时踩过的坑(https://blog.thinkmoon.cn/post/191-ai-cost-optimization-computing-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。