AI 推理服务生产化实战:从 Jupyter Notebook 到高可用推理集群

每位 ML 工程师都经历过那个尴尬时刻——模型在 Jupyter Notebook 里跑出了惊艳的结果,但真正推向生产环境时却举步维艰。本文不讲模型架构,只讲那些教科书里不会教、但决定生死的工程细节。

一、那个"它在我机器上能跑"的幻觉

在 Notebook 里,推理代码通常长这样:

from transformers import AutoModelForCausalLM, AutoTokenizer

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-70B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3-70B")

inputs = tokenizer("请帮我分析这段代码", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=512)
print(tokenizer.decode(outputs[0]))

这段代码隐藏了至少十三个生产级问题:模型加载占用 140GB 显存且需要 80 秒、generate() 是同步阻塞的、没有并发隔离、没有健康检查、没有优雅退出、没有重试限流、没有可观测性、没有 A/B 测试框架、没有模型热更新、没有降级策略、没有输入校验、没有输出过滤、没有资源配额管理。

从 Notebook 到生产,不是加个 Flask 包一层 HTTP 就完事。我们需要的是一个完整的推理平台(Inference Platform)——覆盖模型生命周期、流量调度、资源治理和持续演化的系统工程。

二、推理服务架构设计:单体还是微服务

2.1 单模型服务的陷阱

最常见的生产反模式是为每个模型部署一个独立服务:

❌ 反模式:每个模型一个 FastAPI 服务
model-a:5001  →  1.5 GB 镜像 × 4 副本 = 6 GB 内存浪费
model-b:5002  →  1.5 GB 镜像 × 4 副本 = 6 GB 内存浪费
model-c:5003  →  1.5 GB 镜像 × 4 副本 = 6 GB 内存浪费

每个服务的基础开销(Python 运行时、框架依赖、监控 Agent)约 1-2GB。如果你有 20 个模型,基础浪费就达 20-40GB。更重要的是,每个服务独立版本管理、独立扩缩容、独立故障域,运维复杂度随模型数量线性增长。

2.2 多模型共享推理引擎

生产级推理服务的正确姿势是共享推理引擎 + 模型热加载:

# 使用 Lance 框架的多模型共享推理池
class InferencePool:
    def __init__(self, max_memory_gb=80, max_batch_size=32):
        self.engine = vLLMEngine(
            gpu_memory_utilization=0.92,
            max_num_seqs=max_batch_size,
            enable_chunked_prefill=True,
            # 关键:开启 Prefix Caching 复用 KV
            enable_prefix_caching=True,
        )
        self.loaded_models = {}
        self.lock = asyncio.Lock()

    async def load_model(self, model_id: str, model_path: str):
        async with self.lock:
            if model_id in self.loaded_models:
                return
            # 热加载:不重启推理引擎,动态注册新模型
            await self.engine.add_model(
                model_id=model_id,
                model_path=model_path,
                max_concurrent_requests=16,
            )
            self.loaded_models[model_id] = {
                "loaded_at": time.time(),
                "request_count": 0,
            }

    async def infer(self, model_id: str, request: InferenceRequest):
        if model_id not in self.loaded_models:
            raise ModelNotFoundError(model_id)
        return await self.engine.generate(
            model=model_id,
            prompt=request.prompt,
            sampling_params=request.sampling_params,
        )

核心设计原则:一个推理进程、多模型共享、隔离调度。vLLM、SGLang、TGI 等引擎已原生支持多模型共享同一个 GPU 进程,通过 KV Cache 的 Page Table 隔离不同模型的显存空间。

2.3 三层分离架构

大型推理平台应遵循三层分离:

┌─────────────────────────────────────┐
│           API Gateway               │
│  限流 │ 认证 │ 路由 │ 灰度分流      │
├─────────────────────────────────────┤
│         Inference Router            │
│  KV Cache Locality-Aware Routing    │
│  Model-Affinity Scheduling          │
├─────────────────────────────────────┤
│        Inference Engine Pool        │
│  vLLM/SGLang/TGI 进程池            │
│  GPU 1  │  GPU 2  │  GPU 3        │
└─────────────────────────────────────┘

关键设计:Router 层需要感知 KV Cache 的亲和性。相同 Prefix 的请求路由到同一 GPU,可将 Prefix Cache 命中率从 40% 提升到 85%+。实测表明,在代码补全场景下,这种做法的 TTFT(首 Token 延迟)降低 60%。

三、批量调度与弹性伸缩

3.1 Continuous Batching 的工程陷阱

很多人以为 Continuous Batching 是 vLLM 的默认配置,但实际上需要精细调优:

# 错误配置:max_num_seqs 设太大导致 OOM
SamplingParams(
    max_tokens=2048,       # 单个请求最大生成长度
)
engine_config = EngineConfig(
    max_num_seqs=256,      # 太大!70B 模型每个序列占 300MB+
    # → 256 × 300MB = 75GB KV Cache,显存直接爆
)

# 正确配置:根据模型大小和 GPU 显存精确计算
# KV Cache 大小 = num_layers × 2 × num_kv_heads × head_dim × seq_len × dtype_bytes
# 对于 Llama-3-70B: 80 × 2 × 8 × 128 × 4096 × 2 = ~1.3 GB per sequence
# A100 80GB × 0.92 利用率 = 73.6 GB
# Model weights = 140 GB (FP16) → 需要用 TP=2
# 留给 KV Cache 的空间 ≈ 20 GB
# max_num_seqs ≈ 20GB / 1.3GB ≈ 15

engine_config = EngineConfig(
    tensor_parallel_size=2,
    max_num_seqs=16,       # 精确计算得出
    max_model_len=8192,    # 硬限制最大序列长度
    enable_chunked_prefill=True,  # 分块预填充,避免长输入阻塞短请求
    max_num_batched_tokens=8192,  # 预填充阶段的 token 预算
)

生产经验:max_num_seqs 不是越大越好。当队列中有大量短请求时,较小的 batch size 反而能获得更高吞吐,因为 GPU 的并行计算单元被更充分地利用。建议设置动态 batch size 策略——TTFT 敏感场景用小 batch,Throughput 优先场景用大 batch。

3.2 基于排队论的自适应扩缩容

传统的 CPU/Memory 指标对推理服务几乎无效——推理服务的核心指标是排队延迟和首 Token 时间。

import numpy as np
from collections import deque

class AdaptiveScaler:
    """基于 M/M/c 排队模型的自适应扩缩容"""

    def __init__(self, target_p99_latency_ms=500, min_replicas=2, max_replicas=20):
        self.target_p99 = target_p99_latency_ms
        self.min_replicas = min_replicas
        self.max_replicas = max_replicas
        self.latency_history = deque(maxlen=60)  # 60 秒滑动窗口
        self.scale_cooldown = 30  # 缩容冷却期

    def update(self, current_latency_p99, current_replicas, time_elapsed):
        self.latency_history.append(current_latency_p99)

        # M/M/c 模型:计算需要的副本数
        # λ: 到达率, μ: 服务率, c: 副本数
        lam = self._estimate_arrival_rate()
        mu = self._estimate_service_rate()

        if lam <= 0 or mu <= 0:
            return current_replicas  # 数据不足,保持现状

        # Erlang-C 公式计算 P(等待 > target)
        # 简化为迭代搜索找到最小 c 使得 P(W > target) < 0.01
        for c in range(self.min_replicas, self.max_replicas + 1):
            pw = self._erlang_c_waiting_prob(lam, mu, c, self.target_p99 / 1000)
            if pw < 0.01:
                desired = c
                break
        else:
            desired = self.max_replicas

        # 扩容快,缩容慢(避免抖动)
        if desired > current_replicas:
            return desired
        elif desired < current_replicas:
            if time_elapsed > self.scale_cooldown:
                return desired
        return current_replicas

    def _erlang_c_waiting_prob(self, lam, mu, c, t):
        """Erlang-C 等待概率的近似计算"""
        rho = lam / (c * mu)
        if rho >= 1:
            return 1.0

        # Erlang-C 公式
        r = lam / mu
        sum_terms = sum(r**k / math.factorial(k) for k in range(c))
        last_term = (r**c / math.factorial(c)) * (1 / (1 - rho))
        c_wait = last_term / (sum_terms + last_term)

        # P(W > t) = C(c, r) × exp(-c×mu×(1-rho)×t)
        return c_wait * np.exp(-c * mu * (1 - rho) * t)

将这个 Scaler 与 Kubernetes HPA 集成,用自定义指标替换掉传统的 CPU 指标:

apiVersion: autoscaling/v2
kind: HPA
metadata:
  name: inference-gateway-hpa
spec:
  scaleTargetRef:
    name: inference-gateway
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Pods
    pods:
      metric:
        name: inference_queue_latency_p99
      target:
        type: AverageValue
        averageValue: 500m  # 500ms
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 10   # 快速扩容,10 秒反应
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15              # 15 秒内最多翻倍
    scaleDown:
      stabilizationWindowSeconds: 300  # 5 分钟冷却期再缩容

四、高可用与故障恢复

4.1 推理服务的"慢死亡"

推理服务最常见的故障模式不是突然崩溃,而是逐渐变慢直到不可用。原因通常是:

  1. KV Cache 碎片化导致显存利用率持续下降
  2. Python GC 在长生命周期进程中积累内存泄漏
  3. CUDA 上下文随模型加载/卸载发生泄漏
  4. 请求队列积压导致级联超时
import gc
import psutil
import torch

class HealthMonitor:
    """推理服务的深度健康检查——不只是检查端口是否存活"""

    def __init__(self):
        self.request_times = deque(maxlen=1000)
        self.error_count = 0
        self.slow_count = 0

    async def deep_health_check(self) -> HealthStatus:
        status = HealthStatus.HEALTHY

        # 1. 显存碎片化检查
        gpu_stats = torch.cuda.memory_stats()
        allocated = gpu_stats["allocated_bytes.all.current"]
        reserved = gpu_stats["reserved_bytes.all.current"]
        fragmentation_ratio = (reserved - allocated) / max(reserved, 1)

        if fragmentation_ratio > 0.3:  # 碎片率超过 30%
            # 触发显存整理:暂停接收新请求,让现有请求完成后 gc
            status = HealthStatus.DEGRADED
            await self._trigger_defragmentation()

        # 2. 请求延迟趋势检测(不只是当前值)
        if len(self.request_times) >= 100:
            recent_avg = np.mean(list(self.request_times)[-50:])
            older_avg = np.mean(list(self.request_times)[:50])
            if recent_avg > older_avg * 1.5:  # 延迟增长 50%+
                status = HealthStatus.DEGRADED

        # 3. 内存泄漏检测
        process = psutil.Process()
        rss_mb = process.memory_info().rss / 1024 / 1024
        if rss_mb > self.memory_threshold_mb:
            status = HealthStatus.UNHEALTHY  # 触发重启

        return status

    async def _trigger_defragmentation(self):
        """显存整理:停止接收新请求 → 等待现有请求完成 → torch.cuda.empty_cache"""
        self.accepting_requests = False
        await asyncio.wait_for(
            self._wait_pending_requests_complete(),
            timeout=30,
        )
        gc.collect()
        torch.cuda.empty_cache()
        # 重新初始化 KV Cache 分配器
        self.engine.reset_prefix_cache()
        self.accepting_requests = True

4.2 模型热更新与灰度发布

生产环境不能容忍"停服更新"。推理服务的模型热更新需要精准的流量切换:

class ModelVersionManager:
    """多版本模型管理,支持灰度发布和快速回滚"""

    def __init__(self):
        self.versions: Dict[str, ModelVersion] = {}
        self.traffic_split: Dict[str, float] = {}  # version → weight

    async def deploy_new_version(self, model_id: str, new_version: str, 
                                  checkpoint_path: str):
        # Phase 1: 加载新版本但不分配流量
        new_engine = await self._spin_up_new_engine(
            model_id, new_version, checkpoint_path
        )

        # Phase 2: 预热——用历史请求回放验证正确性
        validation_passed = await self._run_smoke_tests(
            new_engine, 
            test_cases=self.load_regression_tests(model_id)
        )
        if not validation_passed:
            raise ValidationError("新版本回归测试未通过")

        # Phase 3: 灰度 5% 流量
        self.traffic_split = {"v_current": 0.95, new_version: 0.05}
        await asyncio.sleep(60)  # 观察窗口

        # Phase 4: 监控关键指标对比
        metrics_ok = await self._compare_metrics(
            baseline="v_current", 
            candidate=new_version,
            metrics=["avg_latency", "error_rate", "output_quality_score"]
        )

        # Phase 5: 全量切换或回滚
        if metrics_ok:
            self.traffic_split = {new_version: 1.0}
            await asyncio.sleep(300)  # 稳定观察 5 分钟
            await self._tear_down_old_engine(model_id, "v_current")
        else:
            await self._rollback(model_id, new_version)

    def route_request(self, model_id: str) -> str:
        """基于权重的随机路由"""
        rand = random.random()
        cumulative = 0.0
        for version, weight in self.traffic_split.items():
            cumulative += weight
            if rand <= cumulative:
                return version
        return list(self.traffic_split.keys())[-1]

关键经验:灰度发布推理服务时,不要只监控延迟和错误率,还需要监控输出质量。一个模型可能在延迟和错误率上表现"正常",但因为量化或微调导致输出风格偏移,这种情况下需要引入自动化输出评估(如用 LLM 作为 Judge)或人工抽查。

五、KV Cache 与缓存架构

5.1 多层级 KV Cache

单一 GPU 显存的 KV Cache 是最大的性能瓶颈。现代推理系统通常采用三级缓存架构:

请求 → 
  [L1: GPU KV Cache]    命中率 85%  延迟 ~0.1ms
    ↓ miss
  [L2: CPU KV Cache]    命中率 60%  延迟 ~5ms(PCIe 传输)
    ↓ miss  
  [L3: NVMe KV Cache]   命中率 95%  延迟 ~100ms(SSD 读取)
    ↓ miss
  [回源: 重新计算]       延迟 ~500ms(取决于 Prompt 长度)

为什么需要 L2 CPU Cache?GPU 显存极其昂贵(A100 80GB 约 10000 美元),而 CPU 内存便宜得多。将不活跃的 KV Cache 换出到 CPU 内存,可以大幅增加可用 Cache 容量。实测中,1TB CPU 内存可以缓存上千个对话的 KV Cache,对多用户系统的缓存命中率提升巨大。

5.2 KV Cache 的 Page 设计

理解 KV Cache 的 Page 机制对性能调优至关重要:

# KV Cache 的 Page 抽象
class KVCachePage:
    """
    一个 Page 存储固定长度(如 16 tokens)的 KV 向量。
    类似于操作系统中的虚拟内存分页:
    - 逻辑序列 → 物理 Page(动态映射)
    - Page 可以被不同序列共享(Beam Search、Parallel Sampling)
    - Page 可以被换出到 CPU 内存
    """

    PAGE_SIZE = 16  # 每个 Page 存储 16 tokens 的 KV

    def __init__(self, num_layers, num_kv_heads, head_dtype):
        # shape: [2, num_layers, page_size, num_kv_heads, head_dim]
        # 2 for key and value
        self.kv_data = torch.zeros(
            2, num_layers, self.PAGE_SIZE, num_kv_heads, head_dtype
        )
        self.ref_count = 0      # 引用计数,用于 Copy-on-Write
        self.is_shared = False  # 是否被多个序列共享

    def acquire(self):
        if self.is_shared:
            # Copy-on-Write: 写之前先复制
            new_page = self.clone()
            new_page.ref_count = 1
            return new_page
        self.ref_count += 1
        return self

    def release(self):
        self.ref_count -= 1
        if self.ref_count == 0:
            # 归还到 Page Allocator 的空闲列表
            pass

class PageAllocator:
    """KV Cache 的 Page 分配器——类比 OS 的 Buddy System"""

    def __init__(self, total_pages, page_size=16):
        self.free_pages = list(range(total_pages))
        self.allocated: Dict[str, List[int]] = {}  # seq_id → page_ids
        self.shared_pages: Dict[int, Set[str]] = {}  # page_id → {seq_ids}

    def allocate(self, seq_id: str, num_tokens: int) -> List[int]:
        pages_needed = ceil(num_tokens / self.PAGE_SIZE)
        if len(self.free_pages) < pages_needed:
            # 触发换出:找到 LRU 的非活跃序列
            evicted = self._evict_lru(pages_needed)
            self.free_pages.extend(evicted)

        page_ids = self.free_pages[:pages_needed]
        self.free_pages = self.free_pages[pages_needed:]
        self.allocated[seq_id] = page_ids
        return page_ids

    def share_pages(self, src_seq: str, dst_seq: str, num_pages: int):
        """Beam Search 中的 Page 共享——Copy-on-Write"""
        shared = self.allocated[src_seq][:num_pages]
        self.allocated[dst_seq] = shared.copy()
        for pid in shared:
            if pid not in self.shared_pages:
                self.shared_pages[pid] = set()
            self.shared_pages[pid].add(dst_seq)

    def _evict_lru(self, pages_needed: int) -> List[int]:
        """基于 LRU 的 Page 换出——考虑 Page 被共享的情况"""
        # 按最近使用时间排序活跃序列
        sorted_seqs = sorted(
            self.allocated.keys(), 
            key=lambda s: self.last_access_time[s]
        )

        evicted = []
        for seq_id in sorted_seqs:
            if len(evicted) >= pages_needed:
                break
            pages = self.allocated[seq_id]
            # 只换出不被共享的页面
            for pid in pages:
                if pid not in self.shared_pages or len(self.shared_pages[pid]) <= 1:
                    evicted.append(pid)
            del self.allocated[seq_id]

        return evicted

这个设计与操作系统中虚拟内存管理的对应关系:Page ≈ 物理页框,Sequence ≈ 进程,Copy-on-Write ≈ fork() 时的共享页。理解这一层,就能理解为什么 vLLM 的 Prefix Caching 如此高效——它本质上是把相同 Prefix 的序列映射到相同的物理 Page,避免了重复计算。

六、成本优化实战

6.1 GPU 利用率公式

推理服务的 GPU 利用率不是看 nvidia-smi 里的百分比,而是看每秒处理的 Token 数与理论峰值的比值:

实际吞吐 (tokens/s)
GPU 效率 = ──────────────────────────────
            峰值显存带宽 × 每 Token 的显存访问量

对于 Llama-2-70B (FP16):
- 每 Token 解码需要读取全部模型权重: 140 GB / TP_size
- A100 显存带宽: 2 TB/s
- 理论峰值吞吐 (TP=2): 2 TB/s ÷ 70 GB/Token ≈ 28.6 tokens/s

这意味着即使是满载的 GPU,计算利用率也可能很低,因为大模型受限于显存带宽(Memory-Bound)。如何优化?

  1. 量化:INT8/FP8 将模型体积减半,直接翻倍吞吐
  2. 投机解码:用小模型预测 + 大模型验证的方式,将 Memory-Bound 转为 Compute-Bound
  3. Multi-LoRA:共享基座模型,只加载差异部分,用同一 GPU 服务多个微调模型

6.2 实例复用 vs 弹性伸缩

一个反直觉的生产经验:预热好的推理实例比冷启动快 50-100 倍。

冷启动一个 vLLM + Llama-3-70B 服务:
  - Docker 启动: 3s
  - Python 初始化: 5s  
  - 模型加载到 GPU: 45s
  - JIT 编译: 8s
  - KV Cache 预分配: 2s
  ─────────────────
  总计: ~63s

预热好的实例接收请求:
  - 首个 Token 延迟: ~200ms

这意味着,对于有流量惯性的服务(工作日白天流量高、夜间低),缩容过多 → 冷启动惩罚可能比维持更多实例 → 浪费算力的成本更高。解决方案是设置"预热缓冲区"——在预测到流量高峰前 5 分钟就提前扩容,而不是等监控报警后再扩。

6.3 多租户成本分摊

在多租户推理平台中,GPU 成本是核心挑战。一个有效的策略是混部低优先级任务(推理)和高优先级任务(训练):

class GPUOvercommitScheduler:
    """GPU 超卖调度器:利用推理峰值与训练峰值的互补性"""

    def __init__(self, num_gpus, overcommit_ratio=1.5):
        self.gpus = [GPU(id=i) for i in range(num_gpus)]
        self.overcommit_ratio = overcommit_ratio  # 允许 1.5x 超卖

    def schedule_inference_job(self, job: InferenceJob) -> bool:
        """推理任务:需要立即分配,通常放在空闲 GPU"""
        for gpu in self.gpus:
            if gpu.free_vram_gb >= job.estimated_vram_gb:
                gpu.allocate_inference(job)
                return True
        # 尝试抢占低优先级训练任务
        preempted = self._preempt_training_job(job.estimated_vram_gb)
        if preempted:
            gpu.allocate_inference(job)
            return True
        return False  # 拒绝,触发排队

    def schedule_training_job(self, job: TrainingJob) -> bool:
        """训练任务:可弹性伸缩,使用被推理任务抢占后的剩余资源"""
        available_gpus = [g for g in self.gpus if g.training_prefetch_allowed]
        if len(available_gpus) >= job.min_gpus:
            # 训练任务设置 Checkpoint,保证被抢占后可恢复
            job.enable_checkpointing(interval_minutes=5)
            for gpu in available_gpus[:job.min_gpus]:
                gpu.allocate_training(job)
            return True
        return False

    def _preempt_training_job(self, needed_vram_gb: float) -> Optional[GPU]:
        """抢占薪资低的训练任务"""
        # 找到最近 Checkpoint 的训练任务,安全回收 GPU
        for gpu in self.gpus:
            if gpu.training_job and gpu.training_job.last_checkpoint_age_sec > 30:
                gpu.training_job.pause_and_checkpoint()
                return gpu
        return None

七、可观测性与排错

7.1 推理服务的黄金指标

不同于普通 Web 服务,推理服务需要关注的指标:

指标 类型 告警阈值示例 含义
TTFT (Time to First Token) 延迟 P99 > 500ms 用户感知到的首次响应速度
TPOT (Time per Output Token) 延迟 P50 > 50ms 生成每个 Token 的速率
Queue Wait Time 延迟 P99 > 100ms 请求在队列中的等待时间
KV Cache 命中率 命中率 < 70% Prefix Caching 是否有效
Batch 填充率 吞吐 < 60% Continuous Batching 效率
每个 Token 显存消耗 资源 > 基线 20% KV Cache 可能碎片化或泄漏

7.2 分布式追踪的特殊挑战

推理服务的难点在于一次请求涉及多个异步阶段:排队 → Prefill → Decode(可能持续数秒甚至数分钟)。传统的 Span 模型不够用:

from opentelemetry import trace

tracer = trace.get_tracer("inference_service")

async def handle_request(request):
    with tracer.start_as_current_span(
        "inference_request",
        attributes={
            "model": request.model,
            "prompt_length": len(request.prompt),
            "max_tokens": request.max_tokens,
        }
    ) as span:
        # 排队阶段
        with tracer.start_span("queue_wait") as queue_span:
            await request_queue.enqueue(request)
            await request.wait_until_scheduled()
        queue_span.set_attribute("wait_ms", request.queue_wait_ms)

        # Prefill 阶段
        with tracer.start_span("prefill") as prefill_span:
            kv_caches = await engine.prefill(request.prompt)
        prefill_span.set_attribute(
            "tokens_per_second", 
            len(request.prompt) / prefill_span.duration
        )

        # Decode 阶段——这是一个长持续时间的 Span
        with tracer.start_span("decode") as decode_span:
            token_count = 0
            async for token in engine.decode(request):
                yield token
                token_count += 1
                # 每秒记录一个 Event,用于监控生成进度
                if token_count % 32 == 0:
                    decode_span.add_event(
                        "decode_progress",
                        {"tokens_generated": token_count, "elapsed_ms": elapsed_ms}
                    )

        # 汇总指标
        span.set_attributes({
            "total_tokens_generated": token_count,
            "time_to_first_token_ms": request.ttft_ms,
            "avg_time_per_token_ms": total_decode_time / token_count,
            "total_duration_ms": total_time,
        })

关键洞察:Decode 阶段的 Span 可能持续数十秒。传统 APM 假设 Span 在毫秒级完成,需要特别处理这种长生命周期 Span——使用 OpenTelemetry 的 Link 或独立的子 Span 来追踪单个 Token 的生成过程。

八、工程哲学:推理平台的"技术债"

最后分享几个用血泪换来的经验:

第一,不要自己写推理引擎。 2024 年很多人觉得自己写的 batch scheduler 比 vLLM 快,结果三个月后发现 KV Cache 碎片化和 Prefix Caching 的边界情况根本搞不定。vLLM、SGLang、TGI 这些引擎的背后是几十人的全职团队在调优 CUDA kernel、内存分配器和调度算法。除非你是头部 AI 公司的 Infra 团队,否则直接用成熟引擎。

第二,把推理服务当作数据库来运维。 推理服务有三个核心特征与数据库高度相似:状态(KV Cache)、事务(请求的完整性)、和持久化(模型权重)。你应当像对待数据库 DBA 一样对待推理服务的运维——建立慢查询日志(慢请求的 Prompt 分析)、死锁检测(循环依赖的模型链)、和容量规划(显存容量告警)。

第三,推理服务的最大敌人是"我不需要这些"。 很多团队在早期阶段觉得监控、灰度发布、自动扩缩容"太复杂"。但当第一次出现凌晨三点被告警叫醒、发现 GPU 全部 OOM 且没有故障转移、花了四个小时才把服务拉回来时,才会意识到这些"非核心功能"其实是生存必需品。


从 Jupyter Notebook 到生产级推理平台,不是技术的跳跃,而是工程思维的跨越。把每一个"能跑"的原型都当作需要精心照料的系统,才能在 AI 真正进入生产环境时保持从容。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部