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 推理服务的"慢死亡"
推理服务最常见的故障模式不是突然崩溃,而是逐渐变慢直到不可用。原因通常是:
- KV Cache 碎片化导致显存利用率持续下降
- Python GC 在长生命周期进程中积累内存泄漏
- CUDA 上下文随模型加载/卸载发生泄漏
- 请求队列积压导致级联超时
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)。如何优化?
- 量化:INT8/FP8 将模型体积减半,直接翻倍吞吐
- 投机解码:用小模型预测 + 大模型验证的方式,将 Memory-Bound 转为 Compute-Bound
- 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 真正进入生产环境时保持从容。

发表评论 取消回复