快速扩缩容
在生产环境中运行 LLM 推理与训练模型完全是两回事。与基于批处理、可预测的训练不同,推理是由实时用户需求驱动的。这种需求往往是突发性的、难以预测的,并且对延迟或停机毫不宽容。
这意味着系统需要在流量高峰期快速扩容,并在空闲时缩容到零以节省成本。这种弹性是效率的根本。
然而,许多组织像对待训练一样对待推理:他们通过长期承诺预先分配固定的 GPU 容量。这通常会导致:
- 过度配置(over-provisioning): GPU 容量浪费,闲置成本高昂。
- 配置不足(under-provisioning): 请求被丢弃、延迟飙升、用户体验糟糕。
- 预算僵化: 刚性支出无法适应真实的使用模式。
为什么无服务器不是银弹
这个扩展问题看起来很熟悉——无服务器计算多年前就解决了它。像 AWS Lambda 这样的平台让按需扩展变得容易,但无服务器并不能很好地适用于 AI 工作负载。原因如下:
- 不支持 GPU: 大多数无服务器平台不支持 GPU。这不仅仅是技术上的疏忽;它根植于架构与实践层面的考量。
- GPU 难以切片: GPU 强大且高度并行,但在同时处理跨不同模型的许多推理任务时,它们不像 CPU 那样灵活。
- 闲置 GPU 成本高昂: 它们是计算世界中的高性能跑车,擅长特定任务但维护成本高昂,尤其是不能持续使用时。
冷启动问题
推理工作负载需要能够快速扩展、控制成本并保持高性能的基础设施。扩展中的一个根本性挑战是冷启动(cold start)。
在容器中部署 LLM 的背景下,冷启动发生在 Kubernetes 节点此前从未运行过某个给定部署时。因此,容器镜像没有被本地缓存,所有镜像层都必须从头拉取并初始化。
这个问题分三个阶段呈现:
- 云资源调配(cloud provisioning): 这一步涉及云服务商分配新实例并将其接入 Kubernetes 集群所需的时间。根据实例类型和可用性,这可能从 30 秒到几分钟不等,对于像 NVIDIA A100 和 H100 这样的高需求 GPU,甚至可能长达数小时。
- 容器镜像拉取(container image pulling): 由于依赖众多且包含自定义库,LLM 镜像比典型的 Python 任务镜像大得多、复杂得多。尽管云服务商宣称拥有多千兆带宽,实际的镜像下载速度往往慢得多。因此,拉取镜像可能需要三到五分钟。
- 模型加载(model loading): 加载模型所需的时间在很大程度上取决于模型的大小。LLM 因其数十亿参数而引入了显著延迟。关键瓶颈包括:
- 从模型中心下载缓慢: 像 Hugging Face 这样的平台并未针对高吞吐量的多部分下载进行优化,使得获取大型模型文件耗时较长。
- 数据流串行: 模型文件通过多个环节传输:远程存储 → 本地磁盘 → 内存 → GPU。这些步骤之间几乎没有或根本没有并行化。每一步都会增加延迟,尤其是对于那些难以缓存或流式传输的大文件。
- 缺乏按需流式传输: 模型文件必须完全下载并写入磁盘后才能开始推理。这引入了额外的 I/O 操作并延迟了启动。
冷启动问题的每个阶段都需要特定的策略来最大程度地减少延迟。
扩缩容指标
为 LLM 推理扩展基础设施不仅仅是对系统负载做出反应。选择合适的指标对于实现响应迅速、高效且经济高效的扩缩容至关重要。
- CPU 利用率(CPU utilization): 它很简单,且有明确的阈值,但对于基于 Python 的工作负载,它并不能反映真实负载。全局解释器锁(GIL)限制了 CPU 并行性,尤其是在多核机器上,这使得该指标对扩缩容决策具有误导性。
- GPU 利用率(GPU utilization): 理论上更相关的指标,但在实践中并不准确。像
nvml这样的工具只要在采样窗口内有任何内核运行——即使是短暂的——就会将 GPU 报告为"已利用"。这并没有考虑批处理或实际吞吐量,导致过早扩容或对容量产生虚假信心。 - QPS(每秒查询数): 在传统 Web 服务中被广泛使用,但对 LLM 推理不太有用。生成式请求在大小和计算成本上差异很大,取决于输入长度和生成的 token 数。因此,QPS 缺乏一致性,很难用于自动扩缩容调优。
- 并发数(concurrency): 该指标表示正在排队或正在处理的活动请求数量,是反映系统负载的理想度量。并发数易于根据批大小配置,并与实际系统需求直接相关,从而实现精确扩缩容。然而,要让并发数起作用,你需要服务框架的支持,以便自动将并发数作为指标进行采集,并将其作为部署平台的扩缩容信号。