Serverless AI 架构实践

Lambda/Cloud Functions 运行 AI 推理、冷启动优化、GPU Serverless(Modal/Replicate)与成本模型分析

引言

Serverless 计算的核心承诺是"按需付费、零运维"。对于 AI 应用来说,这意味着不需要为 GPU 实例 7x24 待命——只在有推理请求时才消耗计算资源。然而,AI 工作负载的特殊性(大模型体积、GPU 依赖、长推理时间)让传统 Serverless 平台面临诸多挑战。

本文探讨如何在 Serverless 架构下高效运行 AI 推理,从传统 CPU Serverless 到 GPU Serverless 的最新实践。

架构模式对比

三种 AI 部署模式

模式 成本模型 延迟 扩缩容 运维 适用场景
常驻服务器 固定月费 最低 手动/慢 高 QPS、延迟敏感
容器 Auto-scaling 按实例时间 自动/中 中等 QPS、可预测
Serverless 按请求+时间 冷启动高 自动/快 低/突发 QPS、成本敏感

Serverless 适用性判断

决策树:我的 AI 应用适合 Serverless 吗?

开始
  │
  ├─ 平均 QPS > 10?
  │   └─ YES → 常驻服务器更经济
  │
  ├─ 突发流量(闲时 0 QPS,峰值 100+ QPS)?
  │   └─ YES → Serverless 是最佳选择
  │
  ├─ 延迟要求 < 100ms?
  │   └─ YES → 需要 Provisioned Concurrency 或常驻
  │
  ├─ 模型 > 10GB?
  │   └─ YES → GPU Serverless (Modal/Replicate)
  │
  └─ 推理时间 > 15 分钟?
      └─ YES → 异步队列 + 长时任务服务

CPU Serverless AI

AWS Lambda 部署轻量模型

适用于小型模型(<250MB)的实时推理:

# lambda_function.py
import json
import os
import boto3
from transformers import pipeline

# Global: loaded once, reused across invocations (warm start)
_classifier = None

def get_classifier():
    global _classifier
    if _classifier is None:
        # Load from EFS mount or layer
        model_path = os.environ.get("MODEL_PATH", "/mnt/models/distilbert-sentiment")
        _classifier = pipeline(
            "sentiment-analysis",
            model=model_path,
            device=-1,  # CPU only
        )
    return _classifier

def handler(event, context):
    body = json.loads(event.get("body", "{}"))
    text = body.get("text", "")

    if not text:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Missing 'text' field"}),
        }

    classifier = get_classifier()
    result = classifier(text, truncation=True, max_length=512)

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({
            "label": result[0]["label"],
            "score": round(result[0]["score"], 4),
        }),
    }

冷启动优化策略

冷启动分解:

总冷启动时间 = 容器初始化 + 运行时启动 + 依赖加载 + 模型加载
                (~500ms)      (~200ms)    (~2-5s)     (~3-30s)

优化手段:

1. 减小包体积
   - 使用 Lambda Layer 分离依赖
   - 只打包必需的 Python 包
   - 用 onnxruntime 替代 pytorch (10x 更小)

2. 模型优化
   - ONNX 格式 (启动更快)
   - 量化模型 (体积更小)
   - 模型存 EFS 而非打包 (避免解压)

3. 预热策略
   - Provisioned Concurrency (预热实例)
   - CloudWatch 定时 ping (保持 warm)
   - SnapStart (JVM, 目前 Python 不支持)

4. 架构优化
   - 模型预加载到 /tmp (10GB)
   - 使用 Container Image 部署 (10GB 限制)
   - ARM (Graviton) 实例 (性价比更高)

Lambda Container Image 部署

# Dockerfile for Lambda container
FROM public.ecr.aws/lambda/python:3.12

# Install ONNX Runtime (CPU optimized, much smaller than PyTorch)
RUN pip install --no-cache-dir \
    onnxruntime==1.18.0 \
    transformers==4.42.0 \
    tokenizers==0.19.0

# Copy model (pre-converted to ONNX)
COPY models/sentiment-onnx/ /opt/models/sentiment/

# Copy handler
COPY lambda_function.py ${LAMBDA_TASK_ROOT}/

CMD ["lambda_function.handler"]
# Build and deploy
docker build -t ai-inference:latest .
docker tag ai-inference:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/ai-inference:latest
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/ai-inference:latest

aws lambda create-function \
  --function-name ai-sentiment \
  --package-type Image \
  --code ImageUri=123456789.dkr.ecr.us-east-1.amazonaws.com/ai-inference:latest \
  --role arn:aws:iam::123456789:role/lambda-execution \
  --memory-size 3008 \
  --timeout 30 \
  --ephemeral-storage '{"Size": 10240}'

GPU Serverless

Modal 实践

Modal 是目前最成熟的 GPU Serverless 平台,支持按秒计费的 GPU 实例:

# modal_app.py
import modal

app = modal.App("llm-inference")

# Define container image with dependencies
image = (
    modal.Image.debian_slim(python_version="3.11")
    .pip_install(
        "vllm==0.6.0",
        "transformers==4.44.0",
        "torch==2.4.0",
    )
)

# Pre-download model to Modal's distributed volume
volume = modal.Volume.from_name("model-cache", create_if_missing=True)

@app.function(
    image=image,
    gpu=modal.gpu.A100(count=1, size="40GB"),
    volumes={"/models": volume},
    timeout=300,
    container_idle_timeout=120,    # Keep warm for 2 minutes
    allow_concurrent_inputs=16,    # Batch processing
    retries=2,
)
def generate(prompt: str, max_tokens: int = 512) -> str:
    from vllm import LLM, SamplingParams

    # Model loaded once per container lifecycle
    llm = LLM(
        model="meta-llama/Llama-3.1-8B-Instruct",
        download_dir="/models",
        max_model_len=8192,
        gpu_memory_utilization=0.90,
    )

    params = SamplingParams(
        temperature=0.7,
        max_tokens=max_tokens,
        top_p=0.9,
    )

    outputs = llm.generate([prompt], params)
    return outputs[0].outputs[0].text

# Web endpoint with streaming
@app.function(
    image=image,
    gpu=modal.gpu.A100(count=1, size="40GB"),
    volumes={"/models": volume},
    container_idle_timeout=120,
)
@modal.web_endpoint(method="POST")
async def inference_endpoint(request: dict):
    result = generate.remote(
        prompt=request["prompt"],
        max_tokens=request.get("max_tokens", 512),
    )
    return {"text": result}

# Scheduled model warm-up
@app.function(schedule=modal.Cron("*/5 * * * *"))
def keep_warm():
    """Ping every 5 minutes to keep container warm."""
    generate.remote("Hello", max_tokens=1)

Replicate 实践

Replicate 提供了更简单的模型部署方式,适合快速原型:

# Using Replicate API
import replicate

# Run a pre-deployed model
output = replicate.run(
    "meta/llama-3.1-405b-instruct",
    input={
        "prompt": "Explain quantum computing:",
        "max_tokens": 512,
        "temperature": 0.7,
    },
)

# Stream output
for token in output:
    print(token, end="", flush=True)
# Deploy custom model on Replicate
# cog.yaml - Replicate's container definition
"""
build:
  gpu: true
  python_version: "3.11"
  python_packages:
    - "torch==2.4.0"
    - "transformers==4.44.0"
    - "accelerate==0.33.0"

predict: "predict.py:Predictor"
"""

# predict.py
from cog import BasePredictor, Input, ConcatenateIterator
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

class Predictor(BasePredictor):
    def setup(self):
        """Load model once during container startup."""
        self.tokenizer = AutoTokenizer.from_pretrained(
            "meta-llama/Llama-3.1-8B-Instruct"
        )
        self.model = AutoModelForCausalLM.from_pretrained(
            "meta-llama/Llama-3.1-8B-Instruct",
            torch_dtype=torch.float16,
            device_map="auto",
        )

    def predict(
        self,
        prompt: str = Input(description="Input prompt"),
        max_tokens: int = Input(default=512, ge=1, le=4096),
        temperature: float = Input(default=0.7, ge=0.0, le=2.0),
    ) -> ConcatenateIterator[str]:
        inputs = self.tokenizer(prompt, return_tensors="pt").to("cuda")

        with torch.inference_mode():
            outputs = self.model.generate(
                **inputs,
                max_new_tokens=max_tokens,
                temperature=temperature,
                do_sample=temperature > 0,
            )

        result = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
        yield result[len(prompt):]

成本模型分析

GPU Serverless 成本对比

场景:每天 1000 次推理,每次平均 5 秒,使用 A100-40GB

方案 A: 常驻 GPU 服务器
  AWS p4d.24xlarge (8x A100):  ~$32/小时
  月成本: $32 x 24 x 30 = $23,040
  利用率: 1000 x 5s / 86400s = 5.8%  (极低)

方案 B: Modal GPU Serverless
  A100-40GB: ~$0.001/秒
  计算时间: 1000 x 5s = 5000s/天
  月成本: 5000 x 30 x $0.001 = $150
  + 冷启动额外时间: ~$50
  月成本: ~$200

方案 C: Replicate
  A100-40GB: ~$0.0023/秒
  月成本: 5000 x 30 x $0.0023 = $345

节省比例:
  Modal vs 常驻:    $200 vs $23,040  → 节省 99.1%
  Replicate vs 常驻: $345 vs $23,040  → 节省 98.5%

成本交叉点分析

日请求量 常驻 (p4d) Modal (A100) Replicate 推荐
100 $23,040/月 ~$20/月 ~$35/月 Serverless
1,000 $23,040/月 ~$200/月 ~$345/月 Serverless
10,000 $23,040/月 ~$2,000/月 ~$3,450/月 Serverless
50,000 $23,040/月 ~$10,000/月 ~$17,250/月 Serverless
100,000 $23,040/月 ~$20,000/月 ~$34,500/月 常驻
500,000 $23,040/月 ~$100,000/月 - 常驻

交叉点大约在日均 70,000-100,000 次请求(每次 5 秒推理),超过这个量级常驻服务器更经济。

混合架构

实际推荐架构

┌──────────────────────────────────────────────────┐
│                   API Gateway                     │
│              (路由 + 认证 + 限流)                  │
└───────────────┬───────────────┬───────────────────┘
                │               │
    ┌───────────▼─────┐  ┌─────▼───────────┐
    │  高频推理 (>10QPS) │  │  低频推理 (<1QPS) │
    │                   │  │                  │
    │  常驻 GPU 服务器    │  │  GPU Serverless   │
    │  vLLM on K8s      │  │  Modal / Replicate│
    │  Auto-scaling     │  │  按需启动          │
    │  min=1, max=8     │  │  min=0, max=inf   │
    └───────────────────┘  └──────────────────┘
// Hybrid routing logic
async function routeInference(request: InferenceRequest): Promise<InferenceResponse> {
  const currentQps = await getRecentQps(request.model, "5m");

  if (currentQps > 10) {
    // Route to persistent GPU cluster
    return callVllmCluster(request);
  }

  // Route to serverless for low-traffic models
  return callModalEndpoint(request);
}

异步推理模式

对于长时间运行的推理任务(视频生成、大批量处理),采用异步模式:

# Async inference with callback
import modal

@app.function(
    gpu=modal.gpu.A100(count=1),
    timeout=600,
)
def batch_inference(items: list[dict]) -> list[dict]:
    """Process a batch of inference requests."""
    model = load_model()
    results = []
    for item in items:
        result = model.generate(item["prompt"])
        results.append({"id": item["id"], "output": result})
    return results

# Client side: submit and poll
@app.local_entrypoint()
async def main():
    # Submit batch job
    call = batch_inference.spawn(items=batch_items)

    # Poll for completion
    result = call.get(timeout=600)
    print(f"Processed {len(result)} items")

总结

  1. Serverless AI 的最佳场景是低频突发流量:日均请求量低但偶尔有峰值的场景,Serverless 可以节省 95%+ 的成本。
  2. 冷启动是最大挑战:通过模型压缩、ONNX 转换、预热策略和容器复用来缓解。
  3. GPU Serverless 正在成熟:Modal 和 Replicate 让 GPU 推理也可以按秒计费,但生态还在快速演进。
  4. 混合架构是务实选择:高频模型用常驻服务器,低频模型用 Serverless,通过统一网关路由。
  5. 成本交叉点要持续跟踪:GPU Serverless 定价在快速变化,每季度重新评估成本模型。

Maurice | maurice_wen@proton.me