AI 服务成本优化:从算力效率到架构设计的全链路降本

cover

一、GPU 算力成本:AI 服务落地的最大经济障碍

AI 推理服务的成本有多高?以一个日均 100 万次推理请求的在线服务为例,使用 4 张 A100-80G 部署 70B 模型,单月 GPU 租赁成本约 6 万元。加上网络、存储、运维等间接成本,月度总支出可能超过 10 万元。而同等 QPS 的传统微服务,月度成本可能仅需 5000 元。

成本优化的核心矛盾是:降低成本往往意味着牺牲服务质量(延迟、吞吐、精度)。架构师的任务是在满足业务 SLA 的前提下,将单位推理成本降到最低。这不是一个单点优化问题,而是涉及模型选型、推理引擎、资源调度、架构设计的系统性工程。

二、AI 服务成本优化机制:从模型层到架构层的全链路降本

AI 服务成本优化需要贯穿模型选型、推理执行、资源调度、架构设计四个层面。下图展示了各层优化策略的成本收益关系:

flowchart TB
    subgraph 模型层降本
        ModelSelect[模型选型 小模型优先]
        Quant[模型量化 INT4/INT8]
        Distill[知识蒸馏 压缩模型]
        Cache[语义缓存 减少重复推理]
    end

    subgraph 推理层降本
        DynBatch[动态批处理 提升GPU利用率]
        SpecDec[投机解码 降低单次推理成本]
        PagedAttn[PagedAttention 减少显存浪费]
    end

    subgraph 调度层降本
        Elastic[弹性伸缩 低峰缩容]
        Spot[竞价实例 降低GPU单价]
        Mix[训练推理混部 分时复用]
    end

    subgraph 架构层降本
        Cascade[级联架构 轻重模型配合]
        Async[异步化 降低在线GPU占用]
        Offline[离线预计算 减少实时推理]
    end

    ModelSelect --> |成本降幅50%-80%| Cost1[单位成本]
    Quant --> |成本降幅50%-75%| Cost1
    Cache --> |成本降幅20%-40%| Cost1
    Elastic --> |成本降幅30%-50%| Cost2[总成本]
    Spot --> |成本降幅50%-70%| Cost2
    Cascade --> |成本降幅40%-60%| Cost1

    style ModelSelect fill:#9f9,stroke:#333
    style Elastic fill:#ff9,stroke:#333
    style Cascade fill:#9ff,stroke:#333
    style Spot fill:#f99,stroke:#333

2.1 模型层降本:选型、量化与缓存

模型选型是成本优化的第一步。很多场景下,7B 模型配合良好的 Prompt Engineering 就能满足需求,无需使用 70B 模型。模型量化是最具性价比的优化手段,INT4 量化可将显存需求降低 75%,直接减少所需 GPU 数量。语义缓存通过复用相似请求的推理结果,可减少 20%-40% 的实际推理调用。

2.2 调度层降本:弹性伸缩与竞价实例

弹性伸缩在流量低谷期自动缩容,可节省 30%-50% 的 GPU 成本。竞价实例(Spot Instance)利用云厂商的闲置 GPU 资源,价格仅为按需实例的 20%-30%,但可能随时被回收。通过在竞价实例上运行可中断的推理任务,配合优雅停机和请求迁移,可大幅降低成本。

2.3 架构层降本:级联架构与离线预计算

级联架构是最有效的架构层降本手段:轻量模型作为第一层处理大部分简单请求,只有复杂请求才路由到重量模型。离线预计算将可预测的推理结果提前计算并缓存,减少实时推理量。

三、生产级 AI 服务成本优化实现

3.1 级联推理架构:轻重模型配合

"""
级联推理路由器——基于请求复杂度的动态路由
为什么用级联架构而非统一大模型?
因为 80% 的请求是简单查询(FAQ、格式转换、短文本摘要),
用 70B 模型处理这些请求是巨大的算力浪费;
7B 模型处理简单请求的质量与大模型差距很小,
但成本仅为大模型的 1/10
"""
from dataclasses import dataclass
from enum import Enum
from typing import Optional

class ComplexityLevel(Enum):
    SIMPLE = 1      # FAQ、短文本摘要、格式转换
    MODERATE = 2    # 多轮对话、代码补全
    COMPLEX = 3     # 数学推理、长文生成、代码审查

@dataclass
class InferenceRequest:
    request_id: str
    prompt: str
    token_count: int
    task_type: str
    complexity: Optional[ComplexityLevel] = None

class CascadeRouter:
    def __init__(self):
        # 模型配置:模型名 -> (成本/千token, 最大token, 适用复杂度)
        self.model_config = {
            "llama-7b-chat": {
                "cost_per_1k_tokens": 0.002,
                "max_tokens": 4096,
                "max_complexity": ComplexityLevel.SIMPLE,
            },
            "llama-13b-chat": {
                "cost_per_1k_tokens": 0.005,
                "max_tokens": 8192,
                "max_complexity": ComplexityLevel.MODERATE,
            },
            "llama-70b-chat-awq": {
                "cost_per_1k_tokens": 0.02,
                "max_tokens": 4096,
                "max_complexity": ComplexityLevel.COMPLEX,
            },
        }
        # 复杂度分类器:基于规则的快速判断
        # 为什么用规则而非 ML 分类器?
        # 因为分类器本身也需要推理,增加额外成本;
        # 规则分类虽然不够精确,但零成本且延迟极低
        self.complexity_rules = {
            ComplexityLevel.SIMPLE: [
                "FAQ", "summary_short", "format_convert",
                "translation", "sentiment"
            ],
            ComplexityLevel.MODERATE: [
                "chat", "code_completion", "rewrite"
            ],
            ComplexityLevel.COMPLEX: [
                "math_reasoning", "code_review", "long_generation",
                "multi_step_reasoning"
            ],
        }

    def classify_complexity(self, request: InferenceRequest
                            ) -> ComplexityLevel:
        """基于任务类型和 token 数量判断请求复杂度"""
        # 规则一:任务类型直接映射复杂度
        for level, task_types in self.complexity_rules.items():
            if request.task_type in task_types:
                # 规则二:长文本提升复杂度等级
                # 为什么长文本提升复杂度?
                # 因为长上下文需要更强的推理能力来维持一致性
                if request.token_count > 2000 and level.value < 3:
                    return ComplexityLevel(level.value + 1)
                return level

        # 默认:中等复杂度
        return ComplexityLevel.MODERATE

    def route(self, request: InferenceRequest) -> str:
        """将请求路由到最经济的模型"""
        if request.complexity is None:
            request.complexity = self.classify_complexity(request)

        # 选择能处理该复杂度的最小模型
        # 为什么选最小模型而非最强模型?
        # 因为模型越大成本越高,选最小满足需求的模型
        # 是成本优化的核心原则
        selected_model = None
        for model_name, config in self.model_config.items():
            if config["max_complexity"].value >= request.complexity.value:
                if config["max_tokens"] >= request.token_count:
                    if selected_model is None:
                        selected_model = model_name
                    # 已找到满足条件的最小模型
                    break

        return selected_model or "llama-70b-chat-awq"

    def estimate_cost(self, request: InferenceRequest) -> float:
        """预估单次请求成本"""
        model = self.route(request)
        config = self.model_config[model]
        input_cost = request.token_count / 1000 * config["cost_per_1k_tokens"]
        # 输出 token 约为输入的 50%(经验值)
        output_cost = (request.token_count * 0.5 / 1000
                       * config["cost_per_1k_tokens"])
        return input_cost + output_cost

3.2 竞价实例管理:优雅处理中断

"""
竞价实例管理器——处理 GPU 实例被回收的场景
为什么竞价实例需要特殊处理?
因为竞价实例可能被云厂商随时回收(通常提前 2 分钟通知),
必须在这 2 分钟内完成请求迁移和状态保存
"""
import time
import threading
from typing import Dict, List, Optional

class SpotInstanceManager:
    def __init__(self, drain_timeout_seconds: int = 90):
        self.drain_timeout = drain_timeout_seconds
        self.instances: Dict[str, dict] = {}
        self._lock = threading.Lock()
        self._draining = set()

    def register(self, instance_id: str, model_name: str,
                 gpu_id: int, endpoint: str):
        """注册竞价实例"""
        with self._lock:
            self.instances[instance_id] = {
                "model_name": model_name,
                "gpu_id": gpu_id,
                "endpoint": endpoint,
                "registered_at": time.time(),
                "status": "serving",
                "active_requests": 0,
            }

    def handle_interruption_notice(self, instance_id: str):
        """
        处理竞价实例中断通知
        为什么收到通知后不立即停止服务?
        因为需要等待进行中的请求完成(优雅停机),
        同时将新请求路由到其他实例
        """
        with self._lock:
            if instance_id not in self.instances:
                return
            self.instances[instance_id]["status"] = "draining"
            self._draining.add(instance_id)

        # 启动排空倒计时
        threading.Thread(
            target=self._drain_and_terminate,
            args=(instance_id,),
            daemon=True
        ).start()

    def _drain_and_terminate(self, instance_id: str):
        """排空实例上的请求后终止"""
        start_time = time.time()

        while time.time() - start_time < self.drain_timeout:
            with self._lock:
                instance = self.instances.get(instance_id)
                if instance and instance["active_requests"] == 0:
                    break
            time.sleep(2)

        # 超时或请求已排空,终止实例
        with self._lock:
            self.instances.pop(instance_id, None)
            self._draining.discard(instance_id)

    def get_available_instance(self, model_name: str) -> Optional[str]:
        """获取可用的竞价实例(排除正在排空的实例)"""
        with self._lock:
            for instance_id, info in self.instances.items():
                if (info["model_name"] == model_name
                        and info["status"] == "serving"):
                    return info["endpoint"]
        return None

    def get_serving_instances(self) -> List[dict]:
        """获取所有正常服务的实例"""
        with self._lock:
            return [
                {"id": iid, **info}
                for iid, info in self.instances.items()
                if info["status"] == "serving"
            ]

3.3 成本监控与预算告警

"""
AI 服务成本监控与预算告警
为什么需要实时成本监控?
因为 AI 推理成本是"温水煮青蛙"——
单次请求成本不高,但累计起来可能远超预期,
实时监控可以在成本失控前及时干预
"""
import time
from dataclasses import dataclass, field
from collections import defaultdict

@dataclass
class CostRecord:
    timestamp: float
    model_name: str
    input_tokens: int
    output_tokens: int
    cost: float

class CostMonitor:
    def __init__(self, daily_budget: float,
                 alert_threshold: float = 0.8):
        self.daily_budget = daily_budget
        self.alert_threshold = alert_threshold
        self.records: list[CostRecord] = []
        self.model_costs: dict[str, float] = defaultdict(float)
        self._lock = threading.Lock()

    def record(self, model_name: str, input_tokens: int,
               output_tokens: int, cost: float):
        """记录单次推理成本"""
        with self._lock:
            record = CostRecord(
                timestamp=time.time(),
                model_name=model_name,
                input_tokens=input_tokens,
                output_tokens=output_tokens,
                cost=cost
            )
            self.records.append(record)
            self.model_costs[model_name] += cost

            # 检查预算告警
            total_cost = sum(self.model_costs.values())
            if total_cost > self.daily_budget * self.alert_threshold:
                self._send_alert(total_cost)

    def get_daily_summary(self) -> dict:
        """获取当日成本汇总"""
        with self._lock:
            total = sum(self.model_costs.values())
            return {
                "total_cost": round(total, 2),
                "budget": self.daily_budget,
                "budget_usage": round(
                    total / self.daily_budget * 100, 1),
                "by_model": {
                    k: round(v, 2)
                    for k, v in sorted(
                        self.model_costs.items(),
                        key=lambda x: x[1], reverse=True)
                },
                "total_requests": len(self.records),
                "avg_cost_per_request": round(
                    total / max(len(self.records), 1), 4),
            }

    def get_optimization_suggestions(self) -> list[str]:
        """基于成本数据生成优化建议"""
        suggestions = []
        summary = self.get_daily_summary()

        # 建议一:大模型占比过高
        # 为什么关注大模型占比?
        # 因为大模型单次成本是小模型的 10 倍以上,
        # 如果大模型调用量占比超过 30%,说明级联路由可能有问题
        total_cost = summary["total_cost"]
        for model, cost in summary["by_model"].items():
            if "70b" in model and cost > total_cost * 0.5:
                suggestions.append(
                    f"大模型 {model} 成本占比 {cost/total_cost*100:.0f}%,"
                    f"建议检查级联路由规则,将简单请求路由到小模型")

        # 建议二:平均单次成本过高
        if summary["avg_cost_per_request"] > 0.01:
            suggestions.append(
                f"平均单次请求成本 {summary['avg_cost_per_request']:.4f} 元,"
                f"建议开启语义缓存或增加量化级别")

        return suggestions

    def _send_alert(self, current_cost: float):
        """发送预算告警"""
        usage_pct = current_cost / self.daily_budget * 100
        # 实际实现:发送到钉钉/飞书/Slack
        pass

3.4 离线预计算:将实时推理转为离线批处理

"""
离线预计算引擎——将可预测的推理转为离线批处理
为什么离线预计算能降本?
因为离线批处理可以使用竞价实例(成本降低 70%),
且批处理的 GPU 利用率远高于在线推理(90% vs 40%)
"""
class OfflinePrecomputer:
    """
    预计算场景示例:
    1. 商品描述生成:新商品上架时异步生成,而非用户访问时实时生成
    2. 推荐理由生成:离线预生成推荐理由缓存,在线直接返回
    3. FAQ 回答:定期批量生成并缓存,在线查询直接命中
    """
    def __init__(self, batch_model_endpoint: str,
                 cache_service: SemanticCache):
        self.batch_endpoint = batch_model_endpoint
        self.cache = cache_service

    def precompute_faq_answers(self, faq_questions: list[str]):
        """批量预计算 FAQ 回答"""
        # 为什么 FAQ 适合预计算?
        # 因为 FAQ 的问题是固定的,答案不会频繁变化,
        # 预计算后在线查询直接命中缓存,零推理成本
        batch_payload = {
            "model": "llama-7b-chat",
            "messages": [
                {"role": "user", "content": q}
                for q in faq_questions
            ],
            "max_tokens": 256,
            "temperature": 0.3,  # 低温度:答案更确定
        }

        # 批量推理:利用竞价实例的高 GPU 利用率
        results = self._batch_infer(batch_payload)

        # 写入语义缓存
        for question, answer in zip(faq_questions, results):
            self.cache.put(question, answer)

    def _batch_infer(self, payload: dict) -> list[str]:
        """调用离线批处理推理端点"""
        # 实际实现:调用 vLLM 批处理 API
        return []

四、架构权衡:AI 服务成本优化的代价与边界

级联架构的代价:级联路由增加了系统复杂度。复杂度分类不准确时,简单请求可能被路由到大模型(浪费成本),复杂请求可能被路由到小模型(质量下降)。需要持续监控各级模型的质量指标和成本占比,动态调整路由规则。

竞价实例的代价:竞价实例的中断不可预测,可能导致服务短暂降级。在 GPU 资源紧张时,竞价实例可能被频繁回收,可用性无法保证。竞价实例不适合延迟敏感的在线推理,更适合离线批处理和预热池。

量化的代价:INT4 量化在复杂推理任务上的精度损失不可忽视。成本优化不能以牺牲核心业务质量为代价。建议对不同任务类型分别评估量化影响,简单任务用 INT4,复杂任务保持 INT8 或 FP16。

离线预计算的代价:预计算只适用于可预测的查询模式。对于实时对话、代码补全等动态场景,预计算无法覆盖。预计算结果的时效性也是问题——数据更新后缓存需要刷新。

适用边界:成本优化适用于日均推理成本超过 1000 元的 AI 服务。对于小规模内部工具,优化收益不足以覆盖开发成本。成本优化的优先级应遵循"先量化、再缓存、后架构"的顺序——量化和缓存的 ROI 最高,架构改造的 ROI 需要更长时间回收。

五、总结

AI 服务成本优化需要从模型层(选型与量化)、调度层(弹性伸缩与竞价实例)、架构层(级联路由与离线预计算)三个维度协同发力。量化是最具性价比的优化手段,INT4 可将 GPU 成本降低 75%;级联架构将 80% 的简单请求路由到小模型,是架构层降本的核心策略;竞价实例将离线推理的 GPU 单价降低 70%。落地路线上,建议先用量化降低单次推理成本,再引入语义缓存减少重复推理,最后通过级联架构和弹性伸缩优化整体成本结构。成本优化不是一次性项目,而是需要持续监控和迭代的长期工程。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐