到目前为止,你很可能已经把自己的第一个 LLM 部署到 Kubernetes 上运行了。
它能够响应请求,延迟表现甚至可能还不错。
但所谓生产环境,并不是“能跑一次”就够了,而是要能够稳定地跑、在规模化场景下跑、并且在负载之下持续跑。
本章讲的,就是从“能跑”走向“可生产”的这一步转变。
本章将讨论:在真实世界场景中,要让 LLM 推理变得稳定且高效,究竟需要具备哪些条件。这里既包括大家预期中的内容,例如参数调优;也包括那些很容易被忽视、却至关重要的方面,例如运行时内存规划、将请求黏性路由到缓存已预热的副本、模型压缩决策,以及那些需要专用网络配置支持的高级拓扑结构。
把模型服务端当作一个普通容器来对待,是一件很诱人的事。似乎只要设几个资源限制、暴露一个 Service,然后就可以收工了。
但 GenAI 工作负载有其独特特征——模型体量巨大、请求成本波动很大、并且高度依赖 GPU——因此必须采用专门的配置方式。你将学习如何有效配置平台,同时避开那些会悄悄侵蚀性能、并迅速烧光 GPU 预算的陷阱。
本章涵盖五个关键领域:
模型与运行时调优
选择、评估、压缩并基准测试模型
自动伸缩
面向 LLM 工作负载的专门伸缩策略
优化 vLLM 启动时间
降低部署延迟
LLM 感知路由
智能化请求分发
解耦式 Serving(Disaggregated Serving)
高级分布式架构
其中,最根本的决策,是如何选择并调优一个适合你用例、同时又不会浪费算力周期的模型。
模型与运行时调优
对于一个团队来说,在开始开发第一个真正基于生成式 AI 的应用时,也许最重要的事情就是选择要使用的模型。
大多数团队起步时会先用像 OpenAI ChatGPT 这样的托管服务,此时可配置项相对有限。然而,在很多场景中,本地部署(on-premise)基础设施是刚需。一旦走到这一步,模型选择就变得至关重要。这个选择要基于许多不同因素,例如任务类型、工作负载类型(即实时推理还是批量推理),以及并发请求数量。
模型大小当然重要,但即使是两个体量相同的模型,也可能采用不同的模型架构与训练技术,结果就是:面对同一个查询,输出可能从非常准确到完全错误,差异巨大。
正因为这项选择如此关键,如何找到一个合理的起点本身就很困难。可用模型数量极多,而且新模型还在不断发布,使得整个选择过程令人望而生畏。
并不存在什么银弹,也不存在一个可以“统治一切”的单一模型;但模型选择过程也没必要把 Hugging Face 上所有模型都纳入考察范围。开发预测式 AI 模型时,一项常见工作就是比较那些针对同一任务训练出来的模型,并依据准确率来进行筛选。因此,对 LLM 来说,找到一个或多个可用于比较其准确性的指标,也同样是有意义的。
传统预测式 AI 模型通常是为解决某个具体问题而训练的。相比之下,LLM 训练于海量数据之上,能够执行多种任务。要想有效比较 LLM,第一步应当是识别出对你的应用最关键的任务,然后再基于该任务选择准确性指标。
这个阶段至关重要,因为它决定了模型选择是否能够建立在具体指标之上,而不是依赖手工测试。这个问题非常复杂,以至于围绕语言模型评测,已经形成了一个完整的研究领域。
语言模型评测
对语言模型的评测可以面向很多不同方面,例如衡量模型掌握知识的广度、衡量模型生成内容时是否避免有毒语言,甚至评估模型在推理任务上的表现。这个定义并不只适用于 LLM;在 LLM 出现之前的许多传统语言模型,也同样遵循这一原则。
语言评测最重要的应用之一,就是通过特定任务来验证模型安全性,例如测量模型的毒性(toxicity)或鲁棒性(robustness)。
目前有很多项目提供了一项或多项评测基准;其中使用最广泛的套件之一,是 EleutherAI 的 lm-evaluation-harness,它内置了超过一百个开箱即用的任务。除此之外还有其他库,而且新的评测技术也在不断被提出,用来测试模型在越来越复杂场景下的表现。
传统上,一个评测任务通常包括一个数据集,其中包含一组输入和输出(通常会采用多项选择题的形式,以便简化分析),以及一个用于计算指标的评测函数。这样的格式使得领域专家非常容易审阅数据集,同时也便于按子主题进行分组,从而更好地刻画模型能力。
每个 benchmark 本质上都是一个流程:它使用一组预定义的问答对去调用目标模型,然后分析结果。因此,跑一次 benchmark 往往要花不少时间——甚至可能长达数小时——而且模型必须已经部署好,所以这件事的成本可能相当高。幸运的是,对于大多数常用模型,你都可以在网上找到排行榜(leaderboard),其中汇总了多个 benchmark 的评测结果,从而便于直接比较。
所谓 leaderboard,就是一张用一个或多个指标来比较不同模型的表格。
与此同时,依赖这类排行榜也会带来安全与可信性上的隐患:如果不在本地重新执行测试,就无法验证公开出来的数据是否真实。一般建议是:先利用 leaderboard 做初步筛选,形成一个候选模型短名单,然后再做进一步分析。
一个不错的起点,是 Hugging Face 网站上的 leaderboard 页面,它把很多排行榜按类别聚合起来,例如数学能力或模型安全性。举例来说,Open LLM Leaderboard 就会在多个 benchmark 上比较模型,包括 MMLU(知识)、HellaSwag(常识推理)以及 TruthfulQA(真实性)。
当你确定了要在本地进一步评估的模型之后,理解评测流程本身是如何工作的就非常重要了。图 4-1 展示了一个评测请求的执行流。
图 4-1 语言模型评测执行流程
由于每个任务执行一次都可能花上几分钟,因此这类评测通常会在自动化流水线里异步运行。比如,TrustyAI 项目为 lm-evaluation-harness 库提供了一个封装,引入了 LMEvalJob CRD 来执行评测任务,并在完成时发出通知。完整示例可以参考 TrustyAI LM-Eval 文档。
除了现有的数百个评测任务之外,你也可以创建自定义任务;但这样一来,就无法方便地利用在线 leaderboard 去比较不同模型,或者同一模型的不同版本。
使用最广泛的 benchmark 之一是 MMLU(Massive Multitask Language Understanding,大规模多任务语言理解) ,因为它的数据集覆盖了来自多个知识领域的大量选择题,并按任务进行分组,主题范围极广,包括抽象代数、高中欧洲历史、高中政府与政治等许多内容。类似的方法后来也被扩展到了多模态模型,形成了 MMMU(Massive Multi-discipline Multimodal Understanding) 。
虽然通用 benchmark 能评估基础能力,但生产级 LLM 应用往往还需要面向特定用例与特定风险的专门评测工具。
例如,RAG 系统(见第 8 章)需要能够同时评估检索质量与生成准确性的指标;而面向安全的部署则需要漏洞扫描,以识别诸如提示注入或有害输出生成之类的风险。有两个框架正是为这些专门需求而设计的:Ragas 提供了专门面向 RAG 应用的评测指标,用于衡量上下文相关性、答案忠实度和检索准确率;而 NVIDIA garak 则是一个漏洞扫描器,用来探测 LLM 的安全弱点,测试其对提示注入、越狱尝试以及有害内容生成的抵抗能力。
模型选择发生在项目生命周期的早期阶段,但评测并不会到此结束。
当你为生产环境优化模型时,同样的评测技术会再次变得至关重要。数据科学家和 AI 工程师一开始会先选模型,但在真正将它大规模运行之前,你还必须对它进行优化。你可以使用量化等技术来压缩模型,也可以收集生产数据,再把模型蒸馏成一个更小的版本。
然而,这两类压缩技术都会修改模型权重,因此会影响整体准确性。于是你必须再次执行评测,以测量其影响。正确应用这类技术后,模型大小可以缩减到原来的一半以下,而在相同硬件上,运行时吞吐量则可能翻倍甚至翻三倍,同时还能基本维持原始模型的准确度。若处理得当,压缩后的模型能够恢复超过 99% 的原始准确率。
语言模型压缩
像 Meta-Llama-3.1-8B-Instruct 这样的 LLM,拥有 80 亿个参数。
这意味着模型中有 80 亿个浮点数,而每个浮点数都会作为神经网络相应输入的权重(乘数)使用。
每个浮点值使用 16 bit 表示,因此仅仅加载这个模型,最低内存需求就达到 16 GB(16 bit × 80 亿)。这类神经网络往往还包含很多层(例如 32 层),每一层神经元的输出都会传递给下一层,这会进一步增加模型整体内存占用。这部分称为 activation(激活值) ,它同样是 16 bit 浮点值。
最后,KV cache 的值以及中间输出结果,也都用 16 bit 精度来表示。关于这一主题的更多细节,可参见“理解 LLM 基础”。
所有这些浮点数共同构成了 LLM 在 GPU 显存中的总内存占用。
用于压缩模型的技术称为 量化(quantization) (见下方侧栏)。
量化(QUANTIZATION)
量化是一组技术的统称,目的是通过使用更低精度的表示方式——例如 8 bit 浮点(FP8)或整数表示(INT8)——来降低总内存占用。
这些技术远不只是简单地对浮点数取整或减少小数位,而是包含了专门设计的机制,用于补偿执行过程中的误差。
仅仅做模型压缩还不够。
如果运行时本身不原生支持量化,它在处理过程中就必须先把压缩后的模型重新转换回 16 bit 精度,才能对神经网络进行求值并生成激活值。量化真正的“甜蜜点”在于:运行时本身具备经过优化的 kernel 实现(GPU 函数),可以端到端地处理量化数据。只有这样,量化才能真正带来可扩展性上的收益。
vLLM 运行时原生支持大多数主流量化技术。在压缩端,它可以配合 LLM compressor 项目;而在运行时端,它内置了多种优化过的 kernel,当检测到模型已量化时,这些 kernel 会被自动启用。
这一研究方向非常有前景,有望让 LLM serving 变得更加具成本效益,甚至可能成为未来模型与运行时的标准做法。但目前仍存在一些关键问题。首先,那些对量化模型高效扩展至关重要的专用 kernel 往往是硬件相关的,因此并不是所有 GPU 都已经支持;其次,一旦量化做错,模型准确率就可能出现断崖式下降。
前一个问题——也就是 GPU 覆盖面不足——随着时间推移或许会逐步解决;但后一个问题则要求更严谨的控制。量化压缩不是无损过程,因此必须在压缩后重新评估模型质量,以确保准确率没有受到过大影响。
已经有不少案例表明,某些模型在被量化到 8 bit 浮点(FP8) 后,依然能恢复超过 99% 的原始准确率。
llmcompressor 库通过内置校准(calibration)简化了整个流程。使用它本身并不复杂,但要真正理解不同参数的含义,仍然需要一定经验(见示例 4-1)。
示例 4-1 使用 llmcompressor 压缩 LLM
from llmcompressor.modifiers.quantization import GPTQModifier
from llmcompressor.modifiers.smoothquant import SmoothQuantModifier
from llmcompressor.transformers import oneshot
# 选择量化算法。在这个例子中,我们:
# * 应用 SmoothQuant,使激活值更容易被量化
# * 使用 GPTQ 将权重量化为 int8(按通道静态量化)
# * 将激活值量化为 int8(按 token 动态量化)
recipe = [
SmoothQuantModifier(smoothing_strength=0.8),
GPTQModifier(scheme="W8A8", targets="Linear", ignore=["lm_head"]),
]
# 使用内置的 open_platypus 数据集执行量化。
oneshot(
model="TinyLlama/TinyLlama-1.1B-Chat-v1.0",
dataset="open_platypus",
recipe=recipe,
output_dir="TinyLlama-1.1B-Chat-v1.0-INT8",
max_seq_length=2048,
num_calibration_samples=512,
)
其中:
recipe定义了要应用的量化流水线,可包含一种或多种技术。W8A8这个 scheme 表示:权重使用 8 bit 量化,激活值也使用 8 bit 量化。- 必须指定一个用于量化校准的数据集。
- 输出目录将包含压缩后的模型,以及提供服务所需的全部配置文件(如
config.json、tokenizer.json等),这样 vLLM 运行时在加载模型时就无需额外自定义参数。
一个良好的实践是:在模型上线到生产环境之前,先使用语言模型评测技术计算原始模型的准确率指标。进一步地,将压缩过程直接集成进 MLOps / LLMOps 流水线,可以为整个系统提供一个优化过的起点,也就不再需要额外的手工配置。
提示(TIP)
模型压缩是一个复杂且高风险的过程。
不当的量化可能会严重影响模型质量,导致准确率下降、幻觉增加,或者输出错误。虽然 llmcompressor 这类工具提供了校准功能,但要实现最优压缩,仍然需要专业经验以及充分评测。
另一种可行思路,是直接使用那些已经由专业机构完成压缩并验证过的模型。
例如,Hugging Face 上的 Red Hat AI 会发布经过精心量化并完成评测的预压缩模型,从而提供可直接用于生产环境的压缩模型,避免因压缩不当带来的风险。
模型性能基准测试
第 5 章将介绍如何观测一个 LLM,以及哪些指标对于跟踪使用情况和系统响应性至关重要,尤其是在聊天机器人这类实时场景下。快速响应非常关键,因为只有这样,才能通过恰当配置伸缩策略、限流策略,甚至在必要时拒绝新请求并返回合理错误信息,来及时解决延迟问题。
无论系统最终会采取何种缓解措施,前提都是:必须先在不同条件和不同工作负载下,测量整个系统的行为表现。
为了有效测量系统行为,你需要专门工具。围绕 LLM,社区已经发展出若干比较完整的基准测试工具,下面我们就来看看。
性能与容量测试并不是什么新鲜事,软件开发早就离不开它。任何成熟的发布流水线都应该包含这类测试;而从端到端视角看,一个被提供服务的模型与普通应用并没有本质差别:它同样是一个接收请求并生成响应的端点。
目前已经有很多性能测试工具可以直接调用一个 endpoint。运行时当然可以用传统压测工具来测试。事实上,LLM 性能测试最初就是这么做的,而且现在有时仍然如此。只是随着时间推移,社区逐渐发展出了更专门化的工具,它们不仅能够计算更适合 LLM 的指标,还能把性能测试和模型评测结合在一起,在不同场景与不同数据集上完成综合测试。
LLM 社区非常活跃,可用工具也很多。其中较为全面的几种包括:
GuideLLM
GuideLLM 项目是专门为评估模型并优化 LLM 部署而设计的工具。
它能够模拟不同类型的工作负载,从而逼近真实世界场景。GuideLLM 既可以作为交互式工具,用于评估某种特定硬件配置下的性能与资源占用,也支持多种速率场景:从同步场景(请求一个接一个顺序执行),到固定并发场景,再到平均速率可配置的泊松分布场景。完成基准测试后,GuideLLM 会生成一份报告,其中包含延迟分布及其他有价值的信息。示例 4-2 展示了如何在命令行中调用 GuideLLM,其输出见图 4-2。
示例 4-2 使用 GuideLLM 运行基准测试
guidellm benchmark \
--target http://127.0.0.1:8000 \
--model mistralai/Mistral-7B-Instruct-v0.2 \
--output-path output_file.json \
--rate-type sweep \
--data 'prompt_tokens=256,output_tokens=128' \
--max-seconds 400
--warmup-percent 0.2
其中:
- 被测试的目标模型必须在执行测试前先完成部署。
- 基准测试结果会存储到这个 JSON 文件中。
- GuideLLM 支持多种 rate type,其中
sweep是默认值。这个选项非常灵活,能覆盖从请求序列到恒定速率等多种场景,即使没有明确给定某种特定工作负载定义,也足以帮助你获得系统的基线数据。 - 为了让测试结果具有实际意义,必须指定与生产环境预期相符的输入长度和输出长度。工具默认会使用本地的一份《傲慢与偏见》文本,但它也支持使用 Hugging Face 上的数据集,或者本地自定义数据集。
图 4-2 一次 GuideLLM 运行的示例输出
MLPerf Inference
要对机器学习模型做正确的性能测试,并不是 LLM 才有的新问题。围绕这一挑战,早已有很多社区在协作:公司、个人贡献者和学术界共同定义工具并发布结果。MLCommons 就是这样一个 AI 工程联盟,它秉持开放协作理念,致力于改进 AI 系统。该组织提供了多种工具,其中之一就是 MLPerf Inference,并且它已经逐步扩展以支持 LLM。评测结果会定期发布在 MLCommons 网站上,分别覆盖数据中心配置(MLPerf Inference: Datacenter)和边缘/设备配置(MLPerf Inference: Edge)。
Inference Perf
Inference Perf 是一个面向 GenAI 推理性能基准测试的工具,由 Kubernetes WG-Serving 小组提出并孵化,由 Kubernetes SIG Scalability 赞助。这个社区项目获得了多家公司的支持。它专门面向生成式 AI,你可以指定任意数据集,以模拟接近真实世界情况的场景。该库既可以在本地运行,也可以部署到集群中,并连接到一个已部署好的模型——前提是该模型暴露了兼容 OpenAI 的 API。
vLLM benchmark suite
对于像 vLLM 这样的推理引擎来说,性能是一个极其关键的维度。vLLM 提供了公开可用的 nightly benchmark job 以及相关说明。它本身不算是一个独立的完整工具,更像是一组脚本,用于测量某个特定模型或某种硬件配置下的性能。
如前所述,虽然传统负载生成器也能使用,但那样你就需要自己实现 LLM 专属指标,例如 首 Token 时间(TTFT, Time to First Token) 和 Token 间延迟(ITL, Inter-Token Latency) 。无论最终选择哪种工具来测量性能,将其集成到 CI/CD 系统中通常都很直接:只需在流水线中创建一个任务,先部署模型,再使用该工具执行测试即可。真正关键的是,要使用与生产集群等价的环境,尤其是在模型本身以及部署模型所用 GPU 数量方面。
把性能测试集成进发布流水线,并将测试结果持久保存下来,对于正确估算集群规模以及学习系统容量上限至关重要。
这些数据对于容量规划,以及配置限流策略和 API 网关,都是关键输入。
vLLM 运行时参数调优
“理解 LLM 基础”一章已经解释了 LLM 推理是如何工作的,哪些指标值得监控,以及 KV cache 对于高效 LLM serving 有多么关键。现在模型已经经过压缩并准备部署,而性能测试得到的整体系统性能数据,也可以用来指导运行时调优。
这一节专门针对 vLLM 运行时,但其中大部分内容对其他 LLM runtime 同样适用。
vLLM 项目的开发非常活跃;几乎每周都会加入新的优化和新的模型支持。每次发布通常也都会改进默认参数,所以在大多数情况下,你并不需要额外调优,它本身就“能工作”。
vLLM 唯一无法轻易自动推断的,是工作负载类型和所分配硬件的特征;而这也正是你可以通过配置来帮助引擎的地方。
尽管 vLLM 的默认配置通常是一个有效起点,但你仍然需要做一些容量分析,以计算 GPU 需要多少 VRAM(详见下方侧栏中的计算指南)。
如何计算模型的内存需求(HOW TO CALCULATE MODEL MEMORY REQUIREMENTS)
有很多不同因素会共同决定内存需求,其中大多数都与模型架构、模型大小以及并发请求数量有关。
驱动模型内存需求的首要因素,是参数数量。一个拥有 80 亿参数(8B)的模型,所需内存自然远低于那些超过 4000 亿参数的超大模型。
在全尺寸模型中,每个参数通常占用 2 字节(16 bit,float16 或 bfloat16) 。通过压缩技术,这个值可以降低到 1 字节(8 bit,FP8 或 INT8) 。将参数数量乘以每个参数的字节大小,就得到了最基础的内存需求。
除此之外,GPU 上还需要留出一些基础设施开销空间,用于装载优化过的 kernel,通常在 300 MB 到 2 GB 之间。
还需要考虑 activation(激活值) 的成本,也就是执行过程中中间状态所占的内存。这部分直接取决于 hidden size 和 层数。这两个值都可以在模型的 config.json 文件中找到(例如 40 层)。如果序列长度较小(例如 512),activation 内存可能只需要 200–300 MB;但随着序列变长,这一成本会显著上升,因为它是平方级增长的。
最后,输出生成还需要一个输出张量,它的成本直接与词表大小、序列长度和 batch size 相关。
以一个 8B 模型为例,假设参数格式为 float16、上下文长度为 2048、batch size 为 1:
基础内存需求约为 16 GB(80 亿 × 2 字节)。通常还需要预留约 1 GB 的额外基础设施内存、约 900 MB 的 activation 空间(假设 hidden size 为 4096),以及约 400 MB 的输出层开销。
这样算下来,总 VRAM 需求约为 17.3 GB,看起来似乎还算合理。
但如果只是把 batch size 提高到 10,以便同时处理多个请求,这个需求几乎会翻倍,超过 28 GB VRAM。
这远不是该主题的全部,只是一个简化示例。新的技术与模型架构不断出现,也会改变内存占用特征。比如,目前正在演进中的 Mamba LLM 架构,就有望显著降低 KV cache 的大小需求。
一些在线工具可以辅助完成这类计算,例如 TitanML 的 Model Memory Calculator。如果你想深入了解其中用到的所有公式,强烈建议阅读 Alexander Smirnov 的相关博客,以及 EleutherAI 的博客内容。
vLLM 运行时会以一种“贪婪”的方式尽可能利用可用资源,以最大化吞吐量。因此,资源越多,性能通常越好。在理想情况下,vLLM 有足够的 GPU 内存把模型完整加载到 VRAM 中,同时还要有足够空间容纳 activation 和 KV cache。这样一来,运行时就不需要驱逐并重算那些后续仍然会用到的数据。
一旦达不到这个条件,首先出现的症状通常就是 Inter-Token Latency 变高,从而导致吞吐量下降。当然,这并不是唯一的识别方式,因为 vLLM 在遇到这类情况时,通常会非常明确地把信息写进日志中。我们会在示例 5-1 中查看 vLLM 的启动日志,而示例 4-3 这里则重点关注内存相关信息。
示例 4-3 vLLM 日志中的内存信息
...
INFO [model_runner.py:1097] Loading model weights took 14.9888 GB
INFO [worker.py:241] Memory profiling takes 0.67 seconds
INFO [worker.py:241] the current vLLM instance can use total_gpu_memory (79.14GiB)
x gpu_memory_utilization (0.90) = 71.22GiB
INFO [worker.py:241] model weights take 14.99GiB; non_torch_memory takes 0.12GiB;
PyTorch activation peak memory takes 1.19GiB; the rest of the memory
reserved for KV Cache is 54.93GiB.
...
WARNING [scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.SWAP
mode because there is not enough KV cache space. This can affect the
end-to-end performance. Increase gpu_memory_utilization or
tensor_parallel_size to provide more KV cache memory.
total_cumulative_preemption_cnt=1
这里需要注意:
- 在 vLLM 启动时,模型加载进 GPU 内存之后,日志会打印出权重占用大小。
- 一个非常有用的日志项会说明:当前 vLLM 实例总共可使用多少 GPU 内存。
- 除了模型权重,运行引擎本身也会占用一部分额外内存。
- Activation 同样会占用内存,而且这里显示的是“峰值”。它取决于一次执行过程中有多少神经网络节点被激活。例如在 Mixture of Experts 架构中,每次只会激活模型的一部分。
- 剩余内存会分配给 KV cache。
- 在模型执行过程中,如果 KV cache 空间不够,vLLM 就会产生日志,说明它不得不把一部分值 swap 到 VRAM 之外。
当分配给 KV cache 的空间不足时,日志会明确给出提示。偶发一次这样的日志未必严重,但如果频繁发生,就很可能需要考虑调优。幸运的是,vLLM 的日志信息非常详细,甚至会在日志中直接给出解决建议。下面这些参数尤其值得关注:
gpu-memory-utilization
该参数默认值为 0.9,表示 vLLM 最多可使用可用 GPU 内存的 90%。
这个设置是为了给 vLLM 设定一个内存上限,从而避免旧版本里当 vLLM 超出已分配 GPU 内存时发生的 OOM 错误。虽然 0.9 原本只是一个安全阈值,但 vLLM 的稳定性已经大幅提高,因此现在通常可以安全地把这个值调得更接近 1.0,从而拿回那额外的 10% 内存。
max-model-len
这个参数非常关键,必须根据具体的 LLM 用例或任务来设定。
KV cache 的大小与上下文长度(输入 prompt 加生成文本)直接对应。调这个值会直接影响内存占用,但如果把上下文长度限制得太狠,模型可能就无法生成足够多的 token 去满足业务需求。例如在 RAG 场景中,往往就需要较大的上下文窗口。
max-num-seqs 或 max-num-batched-tokens
vLLM 会将输入进行 batch 化,以提高 GPU 利用率和整体吞吐量。
这可能会对延迟有一定影响,但在高并发生产场景里,这种影响通常非常有限。与此同时,batch 越大,KV cache 所需空间也越大,因此如果想节省一些内存,可以考虑适当降低这一参数。
tensor-parallel-size
与前几个参数不同,增加 tensor parallel size 需要额外硬件支持。
这种方式会把模型权重切分到多个 GPU 上,从而让每块 GPU 为 KV cache 腾出更多可用内存。它确实需要多块 GPU,但在同一节点内的跨 GPU 通信通常不是瓶颈,因为有高速互联接口来避免通信延迟。若是多节点 GPU 部署和更高级的网络拓扑,请参考第 7 章中的网络优化与拓扑感知调度。
pipeline-parallel-size
流水线并行会把模型层分布到多块 GPU 上,而 tensor 并行是切分单个 tensor。
两者可以兼容使用,也都能增加可用于 KV cache 的总内存;不过在同节点多 GPU 推理场景下,更常见的做法仍然是 tensor 并行。
data-parallel-size
与流水线并行类似,这种方式会把数据拆分到并行组中,使得在不同服务器上的 GPU 之间实现多节点 serving。当集群中存在多台带 GPU 的服务器时,这种技术就可以使用。它也能增加可用 KV cache 总量,但同时会引入多节点场景的复杂性,因此需要专门的网络连通性保障。
cpu-offload-gb
该参数允许把部分模型卸载到 CPU 上,从而使得那些大于可用 GPU 内存的模型也能部署起来。
看上去这似乎有助于 KV cache 管理,但它会显著拖慢吞吐量。由于性能下降极其明显,并且会失去很多关键的 GPU 专属优化(例如高效 LLM serving 所依赖的专用 kernel),因此强烈不建议在生产环境中使用。
理解工作负载特征,并结合这些参数进行调优,是让运行时性能最大化、并匹配预期服务水平目标的关键。当这些手段仍不足以满足需求时,可以继续往系统里增加更多 GPU;同时,也可以借助 Kubernetes 栈中的其他优化,尤其是网络层面的优化,这部分会在“LLM 感知路由”一节展开。
自动伸缩(Autoscaling)
你已经优化了模型,也调好了运行时参数。
但生产部署还会面临另一个挑战:工作负载并不是恒定的。即便单实例已经调得很好,最终你仍然会在流量高峰期需要多个副本来分担压力。
在实时推理场景中,最终用户延迟极其关键。
因此,应重点监控 TTFT(首 Token 时间) 和 ITL(Token 间延迟) ,并以此指导路由与伸缩决策。
而在离线推理场景下,则更应关注 batch size 和吞吐量调优。
Kubernetes 原生提供了水平 Pod 自动伸缩(Horizontal Pod Autoscaling, HPA),可以在不同副本之间动态平衡请求。
但 LLM 工作负载有其特殊挑战:请求成本会随着 token 数量大幅波动,GPU 利用率又与 CPU / 内存并不直接相关,而且实例启动时间往往较长。类似的问题在分布式训练工作负载中也存在,因此需要更复杂的调度策略,例如 gang scheduling 和配额管理,这些会在第 7 章介绍。
对于自动伸缩,主要有以下几种选择:
Horizontal Pod Autoscaler(HPA)
默认 HPA 开箱即用,也不依赖额外组件。
但它主要监控的是 CPU 和内存,因此并不适合 LLM 这种主要消耗 GPU 的工作负载。这也意味着,LLM 更需要一种更加灵活的自动伸缩方案。
Knative Pod Autoscaler(KPA)
Knative Serving 项目提供了 KPA,它是一种更灵活的 autoscaler,其决策依据是请求数量。
默认的 “stable” 模式会用一个时间窗口来计算并发度,从而决定是否扩容;此外还提供 “panic” 模式,它使用一个更短的时间窗口,以便更快响应负载变化。
与 HPA 相比,KPA 的确更适合 LLM,也能与 KServe(在使用 Knative 部署模式时)原生集成。
但问题在于,KPA 最初是为微服务设计的,而微服务的 Pod 通常可以快速扩容;LLM 部署则要复杂得多,而且依赖 GPU,加载时间往往按分钟计算,具体还取决于模型大小。因此,如果不做大量调优,这种“动态伸缩”在现实中就并不实用(参见“优化 vLLM 启动时间”)。
更重要的是,像 KPA 这种基于请求数量的方法,没有考虑到:请求数并不能直接等价于运行时负载。一个请求可能会生成很多 token,而下一个请求可能只生成极少几个 token。
Kubernetes Event-driven Autoscaling(KEDA)
KEDA 最初是为事件驱动型工作负载设计的。
在这种场景中,请求并不是 HTTP 请求,而通常是来自消息队列的一条消息。KEDA 设计者当时要解决的问题,其实和 LLM 自动伸缩面临的问题很相似:事件驱动架构背后有很多不同技术栈,而每一种系统在暴露整体压力指标方面的方式都不同,因此必须有一个灵活 API,允许用户定义如何采集这些信息。
KEDA 的解决方案,就是支持通过指标查询来指导伸缩。
因为无论哪种消息队列系统,通常都会暴露一些指标,例如待处理消息数量;只是不同技术的风格不同——有的基于 push,有的基于 poll,命名也不一致。
这种灵活性同样非常适合 vLLM,因为 vLLM 会暴露诸如 vllm:num_requests_waiting 之类的指标,用于衡量仍在等待处理的请求数;也会暴露 vllm:time_to_first_token_seconds / vllm:time_per_output_token_seconds,用于跟踪每个 token 的生成耗时。
KServe 原生支持 KPA(在 Knative 部署模式下),同时也支持 KEDA(在 Standard 部署模式下)。
有关 KServe 不同部署模式的更多信息,可回看“KServe”一节。
KEDA autoscaler 的配置写在 InferenceService 规范中,需要定义一条查询语句来决定如何伸缩。它既支持直接从 PodMetric 拉取指标,也支持从外部源获取。
如果查询仅限于 Pod 本地暴露的 vLLM 指标,那么这两种方式效果是等价的;不过在这种场景下,直接从 Pod 获取指标会减少 autoscaler 的响应延迟,使其更灵敏。
而使用外部源则更灵活,因为它可以从多个来源(甚至多个 vLLM 副本)汇总指标,并执行更复杂的联合查询(见示例 4-4)。
示例 4-4 KServe 与 KEDA 配置示例
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: Meta-Llama-3-8B
annotations:
serving.kserve.io/deploymentMode: Standard
serving.kserve.io/autoscalerClass: "keda"
sidecar.opentelemetry.io/inject: "Meta-Llama-3-8B"
spec:
predictor:
model:
modelFormat:
name: huggingface
args:
- --model_name=llama3
- --model_id=meta-llama/meta-llama-3-8b
minReplicas: 1
maxReplicas: 5
autoScaling:
metrics:
- type: PodMetric
podmetric:
metric:
backend: "opentelemetry"
metricNames:
- vllm:num_requests_running
query: "vllm:num_requests_running"
target:
type: Value
value: "4"
# - type: External
# external:
# metric:
# backend: "prometheus"
# serverAddress: "http://prometheus.url:9092"
# query: "vllm:num_requests_running"
这里的含义是:
- 要使用 KEDA autoscaler,必须采用 Standard 部署模式;Knative 模式使用的是它自己的 KPA。
- 这一 annotation 用于启用 KEDA autoscaler。
- 一种从 Pod 本地收集指标的方法,是通过 OpenTelemetry sidecar collector。
PodMetric类型表示:KEDA 将直接去 Pod 查询指标。- 由于 OpenTelemetry 与 Prometheus 在协议上存在差异,因此必须显式指定 backend。
- KEDA 执行的 query 可以是一个简单值,也可以是更复杂、符合 PromQL 语法的查询表达式。
- 这个例子里,值
4表示:如果根据某次前期 benchmark 的延迟分布结论可知,vLLM 同时运行请求数一旦达到 4,就需要扩容,那么 KEDA 就会在 1 到 5 个副本这个范围内增加副本数。 - 被注释掉的那部分配置,演示的是另一种方式:从外部指标源查询数据。
- 如果使用
External类型,就必须提供执行查询的服务地址。
LLM serving 在 autoscaling 方向上仍在持续演进,尤其是在更复杂的场景中,例如解耦式预填充(disaggregated prefill) ,新的定制伸缩技术正在不断出现。
“解耦式 Serving”一节中会介绍解耦式预填充,它与传统 Kubernetes autoscaler 定义差异非常大。它更像是一个“运行时控制器(runtime controller)”:持续观察系统状态,并自动重新平衡不同副本角色,或者按角色调整副本数量。
另一种正在出现的思路,是 llm-d 的 Workload Variant Autoscaler(WVA) ,它专门针对 LLM 工作负载设计。
WVA 并不是只看 CPU 或请求数,而是会评估每个 Pod 实际能处理多少工作,同时考虑到不同请求需要的工作量也不一样(有的会生成很多 token,有的只生成很少),并以实际延迟目标为依据来进行伸缩。这样一来,你就可以在保持性能目标不变的前提下,让集群在更高利用率下运行,再决定是否新增 Pod。WVA 是 llm-d 项目的一部分,我们会在“解耦式 Serving”中再提到它。
本节讨论了 LLM 部署中设计自动伸缩策略的难点。
而当系统中存在多个副本之后,还会出现另一个必须重视的问题:负载均衡策略本身也需要调优。如果负载均衡器对 LLM 毫无感知,那么它就可能造成不理想的请求分发,进而破坏整体延迟的稳定性。这个问题会在“LLM 感知路由”中继续展开。
优化 vLLM 启动时间
在“自动伸缩”一节中,我们已经知道,可以为 LLM 引入更专门的 autoscaler 配置。
但在真实世界里,如果新副本启动要花上好几分钟,那么动态伸缩根本就不现实。
LLM 的体量之大,实际上已经在挑战 Kubernetes 本身的一些核心设计原则:最大的那些 LLM,光是模型文件本身就可能接近 1 TB 存储空间。它们真的非常大。
这使得“检测到流量高峰就立刻横向扩容”这件事变得异常困难。
因为数据传输时间存在物理瓶颈:那么大的模型,不可能瞬间搬过去。
因此,在应用正式部署到生产环境之前,做好容量规划,并完成性能与可扩展性测试,始终是一个好习惯;而对于 LLM 来说,这一点尤其重要。
优化模型加载时间是一项多阶段工作,首先从模型打包方式开始。这部分在第 2 章已有详细介绍;本节则聚焦于 Kubernetes 中从创建部署到运行时真正可以处理请求之间所经历的几个阶段:
1. 运行时镜像的准备(Runtime image provisioning)
和其他传统 Kubernetes 工作负载一样,vLLM 运行时也被封装在容器中,需要先从镜像仓库拉取到 Kubernetes 节点。
vLLM 镜像本身并不小,通常会有几个 GB(一般小于 5 GB);其中大部分体积来自 GPU 框架,比如 NVIDIA 场景中的 CUDA,因此这部分并不能省掉。
默认情况下,镜像是 Pod 需要时才临时从 registry 下载到节点上的。这个行为的频率由 imagePullPolicy 控制,关键点在于:不要把它设置成 Always,否则每次新副本启动或每次重新部署时,都可能重新下载镜像。
在生产环境里,通常推荐使用 IfNotPresent,这样镜像只会在节点第一次需要它时才下载;或者甚至设为 Never,前提是你已经提前把镜像预拉取到了节点上。
当镜像已经存在于节点上时,加载它所需时间其实很短,因此在这一步真正要注意的就是:避免使用 Always。
另外,对于生产部署,还建议使用明确版本号标签(例如 vllm/vllm-openai:v0.12.0),而不是像 latest 这样的可变标签;更进一步的最佳实践,是直接按 digest hash 固定镜像(例如 vllm/vllm-openai@sha256:abc123...),从而保证在所有环境中部署的都是完全同一个镜像版本。
2. 模型获取与挂载(Model retrieval and mounting)
模型的存储与获取方式,会显著影响加载性能;如果设计失当,它甚至会成为整个启动过程的首要瓶颈。
常见方案包括:运行时直接从 Hugging Face 下载、从兼容 S3 的对象存储拉取、先复制到 PVC 再挂载为卷,或者直接封装为 OCI 镜像。
在这些方式中,直接从 Hugging Face 下载和从 S3 获取通常效率都不高,因为它们往往要花很多分钟来完成数据传输与本地拷贝;而 PVC 和 OCI 方式则可以避免这一步额外拷贝,因为模型可以直接以 volume 或 sidecar container 的形式挂载进来。
如果由于某些特殊要求,你必须使用 Hugging Face 或 S3,那么强烈建议启用 KServe 的 local model cache。
这个功能会使用模型的 storageUri 作为 key 来配置本地缓存;如果再结合 SSD 尤其是 NVMe 这类高速存储,效果会更好。
借助 KServe local model cache,使用 Hugging Face 或 S3 的性能就能够接近使用 PVC 的效果。
类似的存储挑战在训练场景中同样存在:训练工作负载需要处理大规模数据集和模型 checkpoint,只不过那边还会多出分布式文件系统与 checkpoint 策略等额外考虑,这些内容会在第 7 章讨论。
3. 启动运行时(Starting the runtime)
vLLM 本身的启动时间通常只需要 1 秒甚至更少。
它会在日志中输出一些很有价值的信息,示例 5-1 里会解释,但就“加载时间”来说,这一步并不是关键路径上的瓶颈。
4. 加载模型(Loading the model)
无论你采用哪种方式来获取和挂载模型,当 vLLM 运行时启动之后,模型都必须已经可用,以便被真正加载。
而在大多数情况下,耗时最多的步骤就是:把模型权重复制到 GPU 显存中。
缩短一个新 runtime 副本从“启动”到“可服务”这段时间,主要就是围绕这一阶段做优化。
这件事存在一个物理上限,也就是 GPU 显存带宽;理论上不可能快过这个上限。但现实中,如果不做优化,模型加载速度通常离这个物理极限还差得很远。
在基础设施层面,像 NVIDIA GPUDirect Storage 这样的技术,可以显著加速数据传输。它能让 NVMe 存储与 GPU 显存之间建立直接通路,绕开 CPU。
而在 vLLM 生态里,目前有三个专门针对“加载时间”问题的扩展项目:CoreWeave Tensorizer、Run:ai Model Streamer 和 fastsafetensor。
优化模型加载(OPTIMIZE MODEL LOADING)
Run:ai Model Streamer 提供了一种高速、高并发的加载实现,用于加载张量;它支持多种文件格式(包括 safetensor),也支持多种存储方式。
CoreWeave Tensorizer 和 fastsafetensor 都要求模型提前使用某种特定序列化格式进行保存,然后才能借此更快加载。
总体而言,在 vLLM 中,无论是 Tensorizer 还是 Model Streamer,配置和使用方式都比较相似(见示例 4-5);而 fastsafetensor 则还需要设置一些额外环境变量。
相比 fastsafetensor,Tensorizer 和 Model Streamer 的社区采用度更高;而 fastsafetensor 更专注于优化从 NVMe 设备加载模型。由于 Model Streamer 不要求重新打包模型,它是最容易上手试验的选项。不过,既然这一章讨论的是生产优化,那么把另外两种方案也纳入考虑,然后根据你的具体环境选择最合适的一种,才更合理。
示例 4-5 vLLM 中使用 Run:ai Model Streamer
vllm serve \
--port=8080 \
--model=/mnt/models \
--served-model-name=meta-llama/Meta-Llama-3-8B \
--load-format runai_streamer \
--model-loader-extra-config '{"concurrency":16}'
这里:
load-format参数使用runai_streamer,无需更换模型序列化格式,但会启用 Model Streamer 的加载逻辑。
如果把它设成tensorizer,则会启用 Tensorizer loader,但前提是模型已经使用 Tensorizer serializer 进行过保存。- 这里的配置表示开启 16 个并发线程 来并行加载模型。其他可选项可以参见 Model Streamer 的环境变量文档。
5. 预热推理引擎(Warming up the inference engine)
当权重已经驻留在 GPU 显存中之后,运行时还需要预分配 KV cache 内存池,并进行一些 profiling 操作。
GPU kernel 是在 GPU 上执行数学运算的专用函数;如果推理过程中要一一单独启动成千上万个 kernel,就会在 CPU 侧形成瓶颈。为了解决这个问题,运行时会把这些 kernel 启动过程捕获为可复用的调用序列,也就是 CUDA(NVIDIA)或 HIP(AMD)graph。
这个“预热”阶段对于实现高吞吐 serving 是必不可少的,但它也会增加整体的 time-to-ready。最近的一些优化工作,主要集中在缓存这些 graph 产物,以及利用 GPU 快照来跳过重复初始化。具体可参阅 vLLM 文档中关于 CUDA Graph 的说明。
6. 暴露模型服务(Exposing the model)
当模型加载完成之后,vLLM 就会暴露出兼容 OpenAI 的 API 以及其他多个 endpoint,并进入可服务状态。
这些 API 中包含一个 /health endpoint,可按照 Kubernetes 最佳实践将其配置为 readiness probe。
以上就是 vLLM 启动运行时、加载模型并对外提供服务时的主要步骤。
其中最耗时的两个阶段分别是:下载模型 和 把模型加载进 GPU。
我们已经解释了如何通过正确配置避免下载耗时,也介绍了如何借助 vLLM 扩展来加速模型加载。另一个建议是:尽可能使用像 NVMe 这样的高速存储。
把这些建议结合起来之后,vLLM 的扩容启动时间就可以从“按分钟计”压缩到“几十秒级”,当然具体效果仍然取决于模型大小。
LLM 感知路由(LLM-Aware Routing)
上一节介绍了如何扩展到多个副本。
而现在,一个新的问题出现了:如何把请求有效地分发给这些副本。
一旦单个副本不足以承载流量、必须启动多个模型副本之后,请求如何在这些副本之间做负载均衡,就会开始对整个集群产生明显影响。
Kubernetes 默认的请求分发策略是 round robin(轮询) 。它会尽可能均匀地把请求分发出去,而这一设计主要是针对传统微服务工作负载——在那种场景里,监控 CPU 和内存通常就足以判断单个副本的负载状况,并据此决定何时扩容。
实际上,即便在云环境的多可用区部署中,round robin 也早已暴露出局限性。
为此,Kubernetes 引入了 topology aware routing(拓扑感知路由) ,用启发式方式尽可能把流量限制在它原本所在的可用区内。
而在 LLM serving 场景下,Kubernetes 的能力边界被拉得更远,因此必须探索不同的路由与负载均衡方式,才能更有效地把请求分配给正确的副本。
要实现面向 LLM 的专门路由,需要综合考虑多个因素:
每个请求都不一样
前面几章已经反复提到这一点。
输入 prompt 的长度,与模型最终会生成多少 token,并不存在必然对应关系;而生成过程也可能持续很多秒。也就是说,一个请求对某个副本造成的负载并不可预测。因此,路由策略必须考虑这个副本当前实际在做多少工作,尤其是还有多少请求正在等待处理。vLLM 会暴露一个专门指标:vllm:num_requests_waiting,用来跟踪这类信息。
Batching
vLLM 会把请求组成一定大小、可配置的 batch,以充分利用所有可用资源并产生更多 token。
这在实时场景中非常关键,因为它可以并行处理更多请求。不过,batch 不一定总能被填满;路由器也可以把离线推理请求和实时请求混合进同一个 batch,以尽量填满它。
Prefill 与 Decode 的负载差异
我们之前已经解释过,prefill 阶段 和 decode 阶段 对硬件的影响完全不同。特别是 prefill 阶段与 prompt 长度直接相关,因此可以专门设计一些 vLLM 实例来处理大 prompt 的 prefill。这种做法称为 解耦式预填充(disaggregated prefill) ,后文“解耦式 Serving”会进一步展开。
KV cache 复用
前面几章已经多次提到 KV cache,它对高效 token 生成极其关键。
但它的价值并不只体现在单个请求内部,还会跨请求发挥作用。
模型本身没有记忆。每个请求对它来说都像是全新的。
例如,在聊天机器人里,每发一条新消息,整个历史对话通常都要重新带给模型,这意味着系统必须再次对之前所有消息做 prefill。
AI agent 的场景也类似:当工具被调用后,再把工具结果连同此前 prompt 一起发回给模型时,同样会重复处理已有上下文。
如果路由器知道每个副本的 KV cache 状态,那么它就可以把请求转发到最可能命中已有缓存的副本上,从而利用这一特性。这种方法称为 prefix-aware routing(前缀感知路由) 。
不同的服务等级要求
实时请求的优先级通常高于批量请求;但对不同用户的请求,优先级策略可能会更复杂。
例如,当系统容量(如等待请求数、KV cache 剩余空间)低于某个阈值时,调度逻辑可以主动丢弃那些不关键的请求,以保证核心请求的服务质量。
LoRA 适配器
还有一个传统 Kubernetes 路由模式不太适用的场景:高效地提供微调模型服务。
模型定制化会在“模型调优”一节详细介绍。一般来说,从 serving 角度看,模型完成微调后,训练任务会产出一个新的专用模型版本,它会被作为一个全新的独立模型来部署。
但如果使用的是 LoRA(Low-Rank Adaptation) 这种技术(见“Low-Rank Adaptation”),那么微调结果会保存为一层较薄的增量层,也就是 LoRA adapter,并与基础模型组合使用。
这种方式允许多个 LoRA adapter 与同一个 base model 一起在同一个 runtime 实例中提供服务,从而节省硬件资源;但它也打破了传统的“一模型对应一个 endpoint”的映射关系。
围绕推理优化与成本下降,目前各方都很关注,也有不同项目正在尝试解决、或者至少改善上述这两类场景。这是一个仍在快速演进的领域。
这些项目的共同目标,是构建一个能够感知 LLM 流量特征的网关组件,并利用这种感知去做优化。图 4-3 展示了这一组件的高层表示。
图 4-3 LLM 感知网关 API
从路由角度看,LoRA adapter 场景其实是较简单的一类,因为只要知道某个 runtime 实例与哪些 LoRA adapter 相对应,就足够完成转发。
真正的难点在于:endpoint 与模型之间不再是一一对应关系,因此集群层面的路由逻辑必须支持这样的服务发现能力,才能把针对某个 LoRA 微调模型的请求转发到真正可提供该模型的地方。
vLLM 原生支持在同一个 endpoint 下同时提供基础模型与 LoRA adapter 服务,并允许用户在请求中直接指定要执行哪个模型。示例 4-6 展示了如何使用 vLLM 提供 LoRA 模型服务。
示例 4-6 使用 vLLM 提供 LoRA adapter 服务
vllm serve meta-llama/Meta-Llama-3-8B \
--enable-lora \
--lora-modules my-lora-model=$HOME/.cache/huggingface/
...
curl localhost:8080/v1/models | jq
{
"object": "list",
"data": [
{
"id": "meta-llama/Meta-Llama-3-8B",
"object": "model",
...
},
{
"id": "my-lora-model",
"object": "model",
...
}
]
}
...
curl localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-lora-model",
"prompt": "LoRA is a",
"max_tokens": 10,
"temperature": 0
}' | jq
其中:
- 基础模型由 vLLM 提供服务。
- 必须通过相应参数启用 LoRA 支持。
--lora-modules用于列出所有需要加载的 LoRA adapter;其中my-lora-model是模型名称,后面跟的是它在容器内的本地路径。由于它本身是一个列表,因此也可以一次加载多个 LoRA adapter,而模型所在目录也可以通过外部挂载卷提供。- 在可用模型列表中,基础模型会被列出。
- LoRA 模型也会作为一个额外可用模型出现在列表中。
- 在实际请求时,可以直接像指定基础模型那样,指定 LoRA 模型名称,因此从终端用户视角看,两者并没有区别。
当 vLLM 被配置为同时提供多个 LoRA adapter 服务之后,下一步就是让 Kubernetes 也“知道”这件事,以便把这类信息用于请求路由。
Kubernetes 生态里已经有一些项目专门用于网络管理,而 Kubernetes Gateway API 正在逐步成为声明式配置各类网关的标准方式。
其中最活跃的社区之一,是专注于模型服务的 Kubernetes SIG / WG-Serving 社区。它孵化了 Gateway API Inference Extension(GIE) 项目,目标正是优化 LLM serving 的路由与效率。
这个项目有时也被称作 Inference Gateway。它扩展了 Kubernetes Gateway API,使之能够理解推理工作负载。
在深入 GIE 技术细节之前,有必要先理解:AI gateway 与传统 API gateway 究竟有什么本质区别,以及它为 LLM 工作负载提供了哪些独特能力。
Gateway API
Gateway API 是一个规模庞大且复杂的项目,聚焦于 Kubernetes 中的 L4 与 L7 路由。
这个官方 Kubernetes 项目的终极目标,是定义并实现下一代 Kubernetes ingress、负载均衡与 service mesh API。
它是一套面向角色(role-oriented) 的 API 设计,用不同 API 表达从基础设施提供方到单个应用暴露的整个网络配置过程。
Gateway API 描述了流量如何被翻译并转发到集群内的服务,但它表达的只是“意图”,并不等价于某个具体 endpoint 或一套完整规范。
路由资源通常是协议相关的,例如 HTTPRoute,它会挂接到某个 gateway 上,并定义将请求转发到某个 service 的规则。
整个规范中还有许多其他对象和概念,但对于解释 Inference Extension 来说并不是关键,因此这里不再展开。更多内容可参考 Gateway API 官网中的 API Overview 页面。
从 API Gateway 到 AI Gateway
传统 API gateway 是为无状态微服务设计的:请求之间彼此独立,不同 endpoint 的资源消耗也大致均匀。
而 AI gateway 则是在这一基础上进一步扩展,加入了专门面向 LLM 工作负载的能力,以应对完全不同的运维挑战。
AI gateway 支持 Model as a Service(MaaS) 架构,也就是把 LLM 推理能力以托管 API 的形式提供给多个用户或多个团队。
类似于 SaaS 通过互联网交付软件能力,MaaS 则是通过配置资源配额、访问控制和使用追踪,把模型推理作为一种服务交付出去,以确保多租户之间的资源公平分配。
基于 token 的限流与用户管理
在多租户环境中为 LLM 提供服务时,必须对用户做追踪,才能真正实现公平访问和配额管理。
不同于传统 API 把“请求”作为计量单位,LLM serving 的基本计算单位是 token。
一个请求可能只生成 10 个 token,也可能生成 10000 个 token,对 GPU 资源和时间的消耗会天差地别。因此,传统按请求次数限流的做法,对 LLM 来说并不有效。
Envoy AI Gateway 提供了基于 token 的限流能力,可以按用户或 API key 追踪和限制 token 消耗量,从而在多个租户共享同一模型部署时实现更公平的资源分配。
类似地,Kuadrant 也提供了与 Kubernetes 原生策略管理集成的 token 限流功能,允许平台管理员按“生成 token 数”而不是“请求数”定义配额。
AI gateway 能力的演进
AI gateway 生态正在迅速从基础的路由与限流,演进到更加复杂的能力:
语义路由(Semantic routing)
一些 semantic router 项目可以根据用户请求的语义内容进行路由,把不同类型的问题发往更合适的专用模型。例如,把代码生成请求发送给代码专长模型,而把普通对话发送给通用模型,从而同时优化成本与效果。
混合路由(Hybrid routing)
更高级的 AI gateway 支持动态地把请求路由到本地自建模型,或者远程云端托管模型,依据可以是当前系统负载、模型可用性以及 SLA 目标。例如,在高峰负载期,可以把溢出流量转发到云端推理节点,同时把对延迟敏感的请求继续留在本地处理。
模型编排(Model composition)
新兴的 AI gateway 模式还支持串联多个模型,让一个模型的输出成为下一个模型的输入,从而实现更复杂的工作流。比如在 RAG 流程中,可以先用检索模型找到相关文档,再交给 LLM 生成最终答案。
这些能力使 AI gateway 不再只是一个简单的流量转发器,而是逐步演化为一个智能编排层,能够在异构模型部署之间同时优化成本、延迟与质量。
Gateway API Inference Extension
Gateway API Inference Extension 项目为 Kubernetes Gateway API 增加了面向 AI 推理工作负载的专门能力,包括:
- 模型感知路由(按模型名称而不是仅按 URL 路径进行路由)
- 服务优先级
- 通过流量拆分实现的渐进式模型发布
它的核心资源是 InferencePool(v1 stable) ,表示一组专门用于提供 AI 模型服务的 Pod,这些 Pod 共享相同的计算配置、加速器类型以及基础模型。
平台管理员可以配置 InferencePool,使其根据 KV cache 利用率、队列长度或模型特征等信息实现智能路由。
另一个资源是 InferenceObjective(alpha) ,用于定义服务目标与路由优先级,从而为不同工作负载提供差异化服务等级。需要注意的是,在 API 演进过程中,InferenceObjective 已经取代了更早的 InferenceModel 资源。
一个常见用例是 LoRA 感知路由:某个 InferencePool 管理着一组 Pod,这些 Pod 为同一个基础模型及多个 LoRA adapter 提供服务,而路由逻辑则会根据 adapter 是否已加载等信息,智能地把请求分发到合适的 Pod。示例 4-7 展示了这些 API 的使用方式。
示例 4-7 Gateway API Inference Extension 使用示例
apiVersion: inference.networking.k8s.io/v1
kind: InferencePool
metadata:
name: vllm-llama3-8b-instruct
spec:
targetPorts:
- number: 8000
selector:
app: vllm-llama3-8b-instruct
endpointPickerRef:
name: vllm-llama3-8b-epp
port: 9002
failureMode: FailClose
---
apiVersion: inference.networking.x-k8s.io/v1alpha2
kind: InferenceObjective
metadata:
name: high-priority-inference
spec:
priority: 1
poolRef:
group: inference.networking.k8s.io
name: vllm-llama3-8b-instruct
---
apiVersion: inference.networking.x-k8s.io/v1alpha2
kind: InferenceObjective
metadata:
name: standard-inference
spec:
priority: 2
poolRef:
group: inference.networking.k8s.io
name: vllm-llama3-8b-instruct
---
apiVersion: v1
kind: Service
metadata:
name: vllm-llama3-8b-epp
spec:
selector:
app: vllm-llama3-8b-epp
ports:
- port: 9002
targetPort: 9002
其中:
InferencePool使用稳定版v1API,所属 group 为inference.networking.k8s.io。targetPorts是一个数组,用于定义模型服务 Pod 暴露的端口,最多可支持 8 个端口。selector用标签匹配来指定哪些 Pod 属于这个池。运行 vLLM 的 Pod 被配置为同时提供基础模型和 LoRA 模型服务,即通过--enable-lora参数启用(见示例 4-6)。endpointPickerRef引用了一个 Endpoint Picker 服务,它通过自定义算法来选择当前最合适的 Pod,以实现智能路由。InferenceObjective用于定义服务目标与优先级,目前仍是 alpha 版本(v1alpha2)。priority字段定义了服务优先级,值越高表示请求越关键,应当被优先处理。- 最后这个 Service,则是实现具体路由逻辑的 Endpoint Picker 服务,它通过 Envoy External Processing 协议工作。
InferencePool 这个概念本身非常灵活,它表示的是一组以推理为目的的 Pod。
路由逻辑可以综合来自不同来源的信息(例如 vLLM 指标),以决定把某个请求发送到哪个 Pod,甚至在当前负载和优先级条件下直接拒绝某些请求。
LoRA 感知路由只是它的一个关键用例:它可以智能地把请求分配给那些已经提供特定 adapter 的 Pod。
Gateway API Inference Extension 的设计目标之一,就是让路由决策逻辑能够不断演化,以更贴近 LLM 工作负载的真实特性。而实现这一灵活性的核心组件就是 Envoy proxy。
Envoy 是一个高可扩展的 HTTP 代理,被广泛用于多种场景,其强大之处在于:可以通过 External Processing(ext_proc) 插入自定义处理逻辑。
External Processing filter 定义了一套 gRPC 协议,允许外部服务被注册为请求处理链中的一个步骤,从而读取并修改 HTTP header 与 request body。
Gateway API Inference Extension 正是利用了这个 External Processing 机制,定义了 Endpoint Picker Protocol(EPP) 。
顾名思义,Endpoint Picker 就是“选择一个 endpoint”的组件。每个 Endpoint Picker 的实现都必须支持 Envoy External Processing 协议,这样 Envoy 才能在处理请求时调用它。
在各种 Endpoint Picker 实现中,一个很有意思的例子是 llm-d 项目中的 inference-scheduler(关于 llm-d,见“解耦式 Serving”一节)。
这个 Endpoint Picker 支持多种过滤器与评分逻辑。例如,它可以周期性抓取不同 vLLM 实例的指标,并使用 vllm:num_requests_waiting 来选择等待队列最短的副本。
从 Kubernetes 视角看,每个 Endpoint Picker 都是一个独立 Deployment,通常会部署在模型所在的同一命名空间中,但这并不是硬性要求。
唯一的要求是:它必须能够访问运行 vLLM 的容器,以便采集指标;同时 gateway 实例(也就是 Envoy proxy)必须能够访问到这个 Endpoint Picker。整个通信链路可以通过 mTLS 或其他证书方式进行安全保护。
在“解耦式 Serving”一节中,Inference Gateway 会与其他组件一起出现在一个更复杂的部署结构中,以分发推理工作负载。
但即使在没有进入那种复杂架构之前,仅仅采用更智能的路由逻辑,也已经能够显著提升系统可扩展性。
当然,Gateway API Inference Extension 并不是唯一选择,也不是唯一一个面向 AI gateway 的开源项目。
Envoy AI Gateway 就是另一个基于 Envoy proxy 构建 AI gateway 能力的项目,它复用了 Gateway API Inference Extension,并在其之上增加了面向用户的功能,例如基于 token 的限流和安全能力。
另一个例子是 vLLM Production Stack 项目,它引入了一个外部可共享的 KV cache 存储层,从而把 KV cache 复用能力扩展到不同实例之间。
最后,还有前面提到过的 llm-d 项目,它在 Gateway API Inference Extension 基础上进一步深化与 vLLM runtime 的集成,包括分布式共享 KV cache 和解耦式预填充。
对于大规模 LLM 部署来说,它是目前最先进的解决方案之一,在“解耦式 Serving”中会被作为参考实现。
虽然 Gateway API Inference Extension 功能强大,但在生产环境中,手动配置 InferencePool、InferenceModel 和 Endpoint Picker 仍然可能非常复杂。
KServe 为此提供了一个更高层的抽象:LLMInferenceService API。它能够自动管理底层 gateway 组件。
同时,LLMInferenceServiceConfig 可以作为一种预设模板,把智能路由、KV cache 感知调度以及解耦式 serving 等常见配置模式封装起来,从而隐藏底层复杂性;如果有需要,也仍然允许用户进行定制(配置示例可参考示例 1-12)。
解耦式 Serving(Disaggregated Serving)
除了 LLM 感知路由之外,还有许多优化可以用来扩大生产环境中的 LLM 服务规模。
而要求越严格——无论是更低延迟还是更高可扩展性——配置就会变得越复杂。
所谓解耦式 Serving,本质上是通过将一个 LLM 感知路由器,与分布式 KV cache 和解耦式预填充(disaggregated prefill) 两种优化结合起来,来实现对 LLM serving 的分布式拆分。
不过,在展开细节之前,必须先强调一点:这种部署方式已经更像是一台“专用设备(appliance)”,而不是一个传统意义上的 Kubernetes 部署。它主要是为那种极大规模的场景设计的——通常一个集群里只服务少数几个模型。
为了实现解耦式 serving,已经有多个项目出现。最知名的两个是 NVIDIA Dynamo 和 llm-d。
两者的主要区别在于侧重点不同:NVIDIA Dynamo 更专精于 NVIDIA 硬件,并且与其硬件深度集成;而 llm-d 则希望支持更多种硬件,并尽可能整合现有开源项目,从而利用整个生态系统的力量。
到目前为止,本章前面介绍的所有内容,都可以应用在一个传统 Kubernetes 集群上,只要集群里至少有一个带 GPU 的节点即可。
但若要真正支持解耦式 serving,这还远远不够。一旦运行时被分布式拆开,特别是当 KV cache 要在不同部署之间共享 时,用来传输 KV cache block 的网络带宽就会成为关键瓶颈,因此必须使用专门的网络配置,才能真正从这种分布式架构中获益。
传统 Pod 网络接口通常基于 Ethernet,带宽一般只有 10–20 Gbps;但解耦式 serving 对网络的需求通常要高一个数量级,大约是 500–600 Gbps!
NVIDIA 为满足这种需求,开发了专门的网络栈。
通过 NVLink 和 NVSwitch,在某些配置下可以让多个 GPU 之间的互联带宽突破 Tbps 限制。
除此之外,还有一些不依赖 NVIDIA 专有硬件的方案,例如基于 RDMA(Remote Direct Memory Access) 和 RoCE(RDMA over Converged Ethernet) 的实现,带宽可以达到 800 Gbps。
其中 InfiniBand 是一个非常著名的 RDMA 实现,它在生成式 AI 之前很多年就已经被用于高性能计算(HPC)领域。而如今,随着生成式 AI 工作负载的发展,这类配置也可能不再局限于超级计算机,而会得到更广泛采用。
在引入了解耦式 serving 所需的额外网络条件之后,接下来可以介绍两个只有在这种集群条件下才真正有意义的优化:分布式 KV cache 和 解耦式预填充。
分布式 KV cache(Distributed KV cache)
KV cache 前面已经提过很多次,因为它是提升 LLM 执行效率的核心机制之一。
分布式 KV cache 的基本直觉其实很简单:如果我们能够把 KV block 存到某个外部缓存里,并在需要时复用它们,不是很好吗?
这种做法有两个主要好处:
第一,KV cache 的大小不再受限于单个实例的可用内存;
第二,不同副本之间可以共享这些 block。
这项优化只有在一种前提下才真正成立:把 KV cache 数据从一个实例传到另一个实例的速度必须足够快,通常需要控制在毫秒级。
这个想法本身很自然,但实现极其复杂。因此,社区里专门围绕这个问题新建了两个项目:LMCache 和 NVIDIA Inference Xfer Library(NIXL) 。
LMCache 给自己的定位是“LLM 领域的 Redis”,它实现了一套用于缓存 KV block 的 API。
正如你所预期的,它最适合的场景,就是那些输入 prompt 很长的情况(例如 RAG),这样一来,新请求就无需在每次到来时都重新执行 prefill。
另一方面,NIXL 则是一个目标更明确的小型库:它旨在加速 AI runtime 中的点对点通信,并对不同类型的内存(例如 GPU、CPU)和存储(从本地文件到远程对象存储)提供统一抽象。
这两个项目可以配合使用。
例如,llm-d 就同时利用了二者的优势。尤其是 NIXL,非常灵活,即使不启用解耦式 serving,也可以单独使用,比如把 CPU 内存当作 GPU 显存的扩展,用来容纳更大的 KV cache。
KV cache 被分布式化之后,也会反过来影响 LLM 感知路由组件。因为即便 KV cache 已经分布式存储并可供所有副本访问,把请求发往那些本地已经准备好所需 KV block 的副本,仍然会更高效,这样可以避免 cache miss 和额外 block 传输。
解耦式预填充(Disaggregated prefill)
Prefill 是请求处理的第一阶段。
在这个阶段,输入 prompt 会被处理,并生成第一个 token。
之后就是 decode 阶段,它会一个 token 接一个 token 地继续生成,直到流式输出结束。
Prefill 是 compute-bound 的,因此它主要影响 TTFT(首 Token 时间) ;
而 decode 是 memory-bound 的,因此它主要影响 ITL(Token 间延迟) (更多细节见“理解 LLM 基础”)。
正因为两者的负载特性截然不同,解耦式预填充 的核心思路就是:把 prefill 阶段和 decode 阶段拆分到两个不同的实例池中,从而允许它们被独立扩缩容、独立调优。
例如,如果你的工作负载中大多数请求都带有很长的输入 prompt,那么处理 prefill 的实例池就需要更多副本。
要启用这种方式,最大的难点在于:prefill 阶段负责初始化 KV cache,因此在把请求交给 decode 阶段继续生成之前,必须先把 KV cache 从 prefill 实例传递给 decode 实例。幸运的是,前面提到的分布式 KV cache 正好可以用在这里。
从实现角度看,解耦式预填充依赖两个条件:
- 分布式 KV cache
- 一个能够感知这一分工的路由组件
现在,构建解耦式 serving 栈所需的关键组件都已经介绍完了,因此我们可以描述这种端到端架构。图 4-4 展示了完整栈:从 Gateway API 到 vLLM,再到 llm-d 的各个组件,以及用于管理部署生命周期的 KServe LLMInferenceService。
图 4-4 llm-d 解耦式 serving 架构
Serving 栈的开发与优化还远远没有结束。
几乎每周都会出现新模型和新模型架构,runtime 侧的开发演进也同样迅速。像解耦式 serving 这样的方案,会让各组件之间的耦合度提高——在超大规模部署中,这是必须付出的代价。
从平台角度看,采用这种方案会让运维复杂度上升;不过 llm-d 社区以及 KServe 等项目,也在持续努力,试图简化部署与生命周期管理。
解耦式 serving 的实现,本身就是多个开源社区协作的成果。
例如,解耦式 prefill 拓扑 最初是由 Mooncake 项目提出的,随后被 NVIDIA Dynamo 与 llm-d 采纳。
这正是开源协作的力量所在。
经验总结(Lessons Learned)
本章探讨了生产级 LLM 推理所需的持续优化工作,涵盖模型选择、运行时配置以及基础设施拓扑等多个维度。
模型选择 不能只看参数规模,也不能只依赖通用 benchmark。
基于领域相关数据集、面向具体任务的评测,往往能揭示通用排行榜掩盖掉的准确性差异。
量化、剪枝等压缩技术可以降低内存占用、提升吞吐量,但质量退化程度会因模型架构不同而有所差异。因此,在真正决定压缩策略之前,必须先用你自己的业务工作负载完成基准测试。
LLM 工作负载的自动伸缩,与传统应用伸缩有本质不同。
像 TTFT、TPOT(每输出一个 token 的时间)以及 KV cache 利用率这样的 token 级指标,通常比请求速率或 CPU 使用率更适合作为伸缩信号。
由于模型加载时间通常以分钟计而非秒计,因此对大多数 LLM 部署来说,scale-to-zero 仍然并不现实。
预热副本,并设置保守的最小副本数,有助于避免冷启动延迟尖刺,因为这种尖刺会直接摧毁用户体验。
vLLM 启动优化 会直接影响部署速度和故障恢复时间。
把模型缓存到持久卷中、优化容器镜像、以及有策略地使用 init container,都可以把初始化时间从“几分钟”降到“几秒钟”。
而这些优化会在滚动发布、自动扩容和故障恢复过程中形成复利效应。
请求路由策略 同时影响延迟与成本。
能够感知缓存的路由机制,会把相似请求转发到同一个副本上,从而最大化 KV cache 命中率,减少重复计算。
而解耦式 serving 架构则会把 prefill 阶段和 decode 阶段分拆到针对不同瓶颈优化的硬件或实例池上,从而允许针对 compute-bound 与 memory-bound 的工作负载进行独立伸缩。
像解耦式 serving 这样的高级拓扑,会引入额外运维复杂度,因此需要配合像 LeaderWorkerSet、拓扑感知调度 以及高带宽网络 这样的专门 Kubernetes 资源与机制。
这些模式在很多方面更像是分布式训练工作负载,而不是传统无状态服务,因此在做基础设施规划时,除了算力容量本身,还必须把 GPU 互联拓扑与网络带宽一并纳入考虑。
当这些优化都落地之后,下一个问题自然就是:你怎么知道它们真的起作用了?
下一章将讨论 LLM 工作负载的可观测性,包括那些能够揭示你的生产环境是否按预期运行的指标、日志与链路追踪。