状态:草稿(2026-10-07),实测文章。 适配器是用作性能测试的 随机权重,因此输出质量没有意义,仅测量了速度和资源行为。计算出的内容标记为“计算”,未测出的内容标记为“未实测”。 编写规则:以实测数据为基础,未测出的内容标记为“未实测”。
目录
- 1. 为什么要服务 LoRA
- 2. 环境与方法
- 3. 吞吐量的成本
- 4. 适配器槽位:当适配器数量超过 --max-loras 时
- 4-1. 在 SGLang 中进行相同的测试
- 5. 运行时加载适配器所需的时间
- 6. 运维经验总结
- 7. 面试常见问题
- 外部依据:通过论文和官方资料确认的内容
- 未能确认的内容
- 参考资料
- 确认的版本与日期
1. 为什么要服务 LoRA
如果为每个用途微调模型,就需要将整个模型加载到内存中。LoRA 的方式是保持基底权重 W 不变,训练两个低秩矩阵(B, A)将其转换为 W + (α/r)·B·A ,因此可以在 单个基底模型上附加多个小适配器 ,并在每个请求中使用不同的适配器。这就是为什么我们要尝试在单张 GPU 上为不同客户和业务提供模型服务。vLLM 文档说明,适配器可以以较低的开销按请求单位进行服务。本文将测试“较低”在此环境中究竟是百分之多少。
2. 环境与方法
| 项目 | 值 |
|---|---|
| GPU | 单张 8GB 级笔记本 GPU,无其他任务 |
| 服务器 | vLLM 0.31.0, Qwen3-1.7B(bf16), --max-model-len 4096, --max-num-seqs 64, --max-lora-rank 16 |
| 适配器 | 使用 PEFT 创建的 3 个 LoRA (a, b, c)。秩为 8,目标是注意力机制的 q、k、v、o 投影, lora_B 使用具有不同随机种子(标准差 0.02)的随机值初始化 |
| 适配器大小 | 有 3,211,264 个可训练参数,占基底模型 1,720,574,976 个参数的 0.19%(测量值)。如果是 bf16 则约为 6.4MB(计算值) |
| 请求 | 输入 Write a long story about a robot.,输出 128 个 Token,温度为 0, ignore_eos,每个请求通过服务器的模型名称选择适配器 |
| 重复 | 一次性发送相同的请求批次,并计算总吞吐量(输出 Token 数 ÷ 耗时) |
请注意并发请求数量。 本文的表格记录的是“一次性发送的请求数”(3 个、18 个、48 个)。由于测试工具的参数名称,最初称其为“并发 1, 16”,但在确认实际发送了 3 倍的请求后进行了纠正。
由于适配器是随机的,输出没有意义。由于适配器矩阵的大小和计算量与是否训练无关, 我们认为速度和内存行为与训练后的适配器相同,但并未在训练后的适配器上进行验证(未实测)。
3. 吞吐量的成本
从基础服务器(关闭 LoRA)和开启 LoRA 的服务器发送了相同的请求。
| 一次性发送的请求数 | 服务器 | 请求使用的模型 | 总吞吐量 | 相对基准 | ITL 中位数 |
|---|---|---|---|---|---|
| 3 | 关闭 LoRA | 基底 | 199 tok/s | 基准 | 15ms |
| 3 | 开启 LoRA | 基底(无适配器) | 200 tok/s | +0.5% | 15ms |
| 3 | 开启 LoRA | 适配器 a | 182 tok/s | −8.5% | 16ms |
| 48 | 关闭 LoRA | 基底 | 2,667 tok/s | 基准 | 17ms |
| 48 | 开启 LoRA | 基底(无适配器) | 2,664 tok/s | −0.1% | 17ms |
| 48 | 开启 LoRA | 适配器 a | 2,190 tok/s | −17.9% | 19ms |
| 48 | 开启 LoRA | 交替使用 a 和 b | 2,323 tok/s | −12.9% | 20ms |
| 48 | 开启 LoRA | 交替使用 a、b 和无适配器 | 2,346 tok/s | −12.0% | 20ms |
- 开启 LoRA 功能本身是免费的。 不使用适配器的请求的吞吐量与关闭 LoRA 的服务器相同(200 比 199,2,664 比 2,667)。
- 使用适配器的请求吞吐量有所下降。 上述单次测量的数值在 3 个适配器时为 8.5%,48 个适配器时为 17.9%,但在下面的“重复测试”中,由于第一次测量结果较低,48 个数值被证明是被夸大了(重复测试后约为 10%)。文档中的“低开销”在此环境中是一个 不可忽视的值。我们将其理解为,在 1.7B 模型中,适配器的附加计算相比权重读取成本(第 21 篇)并不算小,但并未分解具体是哪种计算占用了多少开销(推测)。
- 最初写的是“混合适配器也没有变差”,但通过重复测试被推翻了。 在单次测量中,仅使用适配器 a 时(2,190)的吞吐量低于混合使用 a 和 b 时(2,323),但在 5 次重复测试中,仅使用 a 的中位数为 2,390,混合使用时为 2,330。单次测量的 2,190 是因为启动服务器后首次调用适配器请求所以速度较慢的值(见下文)。
- ITL 中位数从 15 增加到 20ms。 延迟增加的幅度与吞吐量的下降方向一致。
- 即使模型变大,比例也没有减少。 由于该模型较小,每个 Token 约 15ms,我们预计适配器的固定成本相对较大。因此,我们启动并测试了两次使用 Qwen3-4B-FP8(每个 Token 约 19ms,使用相同方式制作的随机适配器,2 个槽位)的服务器,发现在并发 1 下,适配器 a 的吞吐量变化在两次测试中均为 −11%(从 155 降至 138 tok/s),在并发 16 下分别为 −18.3% 和 −10.7%。−18.3% 的一侧是 KV 缓存为 6,320 个 Token 的服务器,−10.7% 的一侧是 KV 缓存为 10,896 个 Token 的服务器(第 14 篇)。在混合使用适配器 a、b、c 的并发 6 测试中,吞吐量为 397 和 396 tok/s,槽位不足导致的性能断崖依然存在。由于精度(fp8)不同,这与 1.7B 并非严格比较。
重复测试(每次 5 次,同一服务器)。 我们将 48 个请求(并发 16 的 3 倍)按无适配器、适配器 a、交替使用适配器 a 和 b 这三种条件打乱顺序,各发送了 5 次(--max-loras 2).
| 条件 | 吞吐量 (tok/s),5次 | 中位数 | 相比基准 |
|---|---|---|---|
| 无适配器 | 2,647, 2,658, 2,669, 2,663, 2,667 | 2,663 | 基准 |
| 适配器 a | 2,183, 2,398, 2,390, 2,391, 2,390 | 2,390 | -10.3% |
| 交替使用适配器 a 和 b | 2,329, 2,330, 2,332, 2,329, 2,332 | 2,330 | -12.5% |
- 只有第一次测量值较低。 适配器 a 的第一个值 2,183 是启动服务器后首次调用适配器的测量结果,随后的四个值(2,390~2,398)则非常稳定。这似乎是因为首次使用适配器时夹杂了加载和初始化成本(未做分解)。这就是前面单次测量的数值看起来被夸大到 17.9% 的原因。这是一个表明基准测试的 首个值应视为预热并应通过重复测试取中位数的典型案例。
- 适配器成本约为 10~12% 。只有一个适配器时的开销略低于混合使用两个适配器(-10.3% 对 -12.5%)。重复测试之间的离散度很小:无适配器为 ±0.4%,适配器 a(排除首个值)为 ±0.2%,a 和 b 为 ±0.1%,因此这种差异具有实际意义。
- 上述 3 个请求测试中的 -8.5%(182 对 199)也是单次测量值,可能混入了预热开销(未进行重复测试)。
4. 适配器槽位:当适配器数量超过 --max-loras 时,
--max-loras 是 单个批次中可以容纳的不同 LoRA 的数量(默认值为 1)。最初,我将其记为“可以同时加载到 GPU 上的适配器数量”,但当我直接阅读本文所使用的 vLLM 0.31.0 镜像的源码(vllm/config/lora.py)时,发现其解释是“Max number of LoRAs in a single batch”,而放置在 CPU 端的适配器数量是单独的 --max-cpu-loras(默认值与 max_loras 相同)。我们用 3 个适配器同时发送了 18 个请求(每个设置重复 2 次。虽然两次的值大多相同,但 2 个槽位的适配器 a 和 b 分别为 886 和 984,有所不同)。
| 服务器设置 | 无适配器 | 适配器 a、b | 适配器 a、b、c |
|---|---|---|---|
--max-loras 2 |
1,120 tok/s | 886~984 tok/s | 529 tok/s |
--max-loras 4 |
1,131 tok/s | 986~987 tok/s | 950 tok/s |
- 如果槽位不足,吞吐量几乎会减半。 在 2 个槽位中交替使用 3 个适配器时,吞吐量降至 529 tok/s,仅为 4 个槽位时的 950 tok/s 的 56%,以及无适配器基准(1,120)的 47%。 我们在源码中确认了原因。 同一镜像的调度器源码(
vllm/v1/core/sched/scheduler.py)显示,当选择等待中的请求时,如果当前批次中包含的不同 LoRA 数量已达到max_loras且该请求的 LoRA 不在批次中,则会跳过该请求并将其放回等待队列(注释为“Scheduling would exceed max_loras, skip”)。也就是说,如果在 2 个槽位中交替出现 3 个适配器,第三个适配器的请求会被不断推迟,导致批次变小,总吞吐量也随之下降。我最初推测的“由于切换适配器导致的等待时间”这一原因,根据源码事实进行了修正。我们并未通过服务器指标读取被推迟请求的等待时间分布以及实际批次大小(未确认)。 - 如果槽位充足,即使适配器数量增加,成本也几乎保持不变。 在 4 个槽位下,2 个适配器(987)与 3 个适配器(950)的差异仅为 4%。
- 因此,
--max-loras应与“在同一时间点的同一个批次中使用的适配器数量”相匹配。 即使有数百个适配器,只要同时出现在请求中的适配器数量在槽位数量之内就没有问题,否则性能可能会下降到上述的一半水平。由于槽位会预先分配适配器权重和计算空间,因此会占用 GPU 内存。在该服务器中,开启槽位的服务器的总内存使用量为 6,144MiB,与未开启的服务器(6,168MiB)相近(这可能是通过相应减少 KV 缓存来吸收的,但我们未确认 KV 容量)。
4-1. 如果在 SGLang 中进行相同的测试
我们使用相同的模型和 3 个相同的随机适配器(rank 16,占参数的 0.19%)对 SGLang 0.5.21 的 LoRA(--enable-lora --max-loras-per-batch N)进行了测试。
| 测试 | SGLang | vLLM(上述第 3、4 节) |
|---|---|---|
| 并发 16(48 个请求),无适配器 | 2,544 tok/s(3 次服务器测试的中位数,2,533~2,552) | 2,664(开启 LoRA 服务器的无适配器请求) |
| 相同负载,适配器 a | 2,515 tok/s(-1.1%,3 次服务器测试的中位数,2,509~2,518) | 2,390 tok/s(-10.3%,5 次测试的中位数) |
| 18 个请求,混合 2 个适配器,槽位 2 | 1,045 tok/s(无适配器 1,027) | 886~984 tok/s(无适配器 1,120) |
| 18 个请求,混合 3 个适配器,槽位 2 | 558 tok/s | 529 tok/s |
| 相同测试,槽位 4 | 1,034 tok/s(3 次服务器测试的中位数,1,030~1,038,首次测试 1,045) | 950 tok/s |
- SGLang 中单个适配器的成本更低(-1.1% 对比 -10.3%). 起初我写道,因为 SGLang 的数值只测了一次,所以可能和 vLLM 一样第一个数值比较低;但在重新启动服务器三次并进行测量后,适配器 a 的降幅几乎相同,分别为 -0.9%、-1.3% 和 -1.1%,并没有出现第一个数值较低的现象。在并发 1 的情况下,吞吐量从 199 降至 194 tok/s(-2.0%~-2.5%)。这暗示了两个引擎的 LoRA 内核与批处理方式有所不同,但我没有去拆解其原因(推测)。由于这些结果是在该模型和 rank 16 适配器下测得的值,因此不将其泛化。
- 槽位不足导致的性能断崖在两个引擎中是一样的(这些值也各测量了一次)。当单个批次中不同的适配器数量多于槽位时,吞吐量几乎下降了一半(SGLang 558 对 1,045,vLLM 529 对 950),而将槽位增加到适配器数量以上时则恢复了。槽位数量不能小于适配器种类数量这一第 4 节的结论,无论使用哪个引擎都是成立的。
- 局限性:由于使用的是相同的随机适配器,它可能与实际训练过的适配器有所不同。SGLang 的单个适配器和槽位 4 的数值是通过三次服务器运行重复测量的,但槽位 2 的断崖(558、529)是仅测量了一次的值。
5. 运行时加载适配器所需的时间
VLLM_ALLOW_RUNTIME_LORA_UPDATING=True 启动服务器后,通过 /v1/load_lora_adapter 添加了适配器 c。
| 项目 | 结果 |
|---|---|
| 加载请求的响应 | LoRA adapter 'c' added successfully,完整调用耗时 0.18 秒(包含远程命令执行开销) |
| 加载后的首批请求 | 3 个请求 182 tok/s,与适配器 a 速度相同 |
- 加载单个适配器(6.4MB)的操作在无需重启服务器的情况下,于 0.2 秒内完成。 这与服务器重启需要 35~55 秒(第 14 篇)形成了对比。适配器部署比模型部署轻量得多,这是 LoRA 服务在运营上的优势。
- 不过文档中警告称不要在生产环境中使用此功能。 vLLM 文档指出,除非运行中更改适配器的环境是被隔离且完全可信的,否则不要在生产环境中使用它。我的理解是,这存在安全风险,因为请求可能会加载任意路径的适配器(这是未经直接验证的解读)。在生产环境中,最好在启动时通过
--lora-modules指定,或者通过流水线来统筹适配器部署,这样更为安全。 - 开启 LoRA 时的服务器准备时间为 70 秒,比未开启 LoRA 的服务器(重启 35~55 秒)更长。开启 LoRA 的服务器捕获了更多形态的 CUDA 图(日志中显示 38 个对 19 个)。
6. 生产环境中的注意事项
- 预先测量使用适配器的请求的吞吐量代价。 在此环境中为 8~18%,具体取决于模型和并发度。
--max-loras将 与同时出现的适配器数量相匹配。如果槽位不足,性能会下降到约一半水平。- 适配器部署虽然轻量,但不受控制地加载存在风险。 应通过流水线进行控制,并且只加载经过验证的适配器。
- 常驻 LoRA 没有额外代价。 对不使用适配器的请求没有影响。
- 必须针对训练过的适配器和更大的模型重新测量。 本文的数值是从小模型(1.7B)、一个 4B-FP8 数据点以及随机适配器中得出的。
7. 面试可能出现的问题
- 为什么要部署多个 LoRA 适配器? 因为它们共享一个基础模型,且仅替换适配器(约为基础模型的 0.19%),所以比为每个用途部署完整模型所需的 GPU 内存要少得多。
- 使用适配器会变慢多少? 在此环境中,对于 1.7B 模型,在 48 个请求的基准下减少了约 10~12%(5 次重复的中位数,最初测得的一次 17.9% 混入了预热因素)。仅保持 LoRA 开启是没有代价的。
--max-loras决定了什么,如果不足会怎样? 它决定了单个批次中可以容纳的不同 LoRA 的数量(默认值为 1)。如果适配器数量超过此限制,调度程序将跳过该适配器的请求,从而导致批次变小。当用 2 个槽位服务 3 个适配器时,吞吐量降至 529 tok/s,仅为 4 个槽位时的 950 的 56%。- 为什么适配器部署比模型部署更容易? 6.4MB 的适配器在无需重启服务器的情况下 0.2 秒即可加载完成。而模型部署则需要 35~55 秒的重启时间加上权重加载时间。
- 为什么不能在生产环境中使用运行时 LoRA 加载? 文档警告称,除非是在隔离的可信环境中,否则请勿使用。
外部依据:通过论文和官方资料确认的内容
由于外部数值的模型、GPU 和适配器配置有所不同,因此不进行直接对比,仅判定方向和数量级是否一致。
| 主题 | 资料与确认位置 | 确认的内容与条件 | 与本文的关系 |
|---|---|---|---|
| LoRA 的大小 | Hu 等人, 「LoRA: Low-Rank Adaptation of Large Language Models」, arXiv 2106.09685 (ICLR 2022), 摘要与第 4.2 节 | 在 GPT-3 175B 中,训练参数最多减少 10,000 倍,检查点从 350GB 缩减至 35MB(秩为 4,仅应用于查询和值投影)。论文在局限性中指出,推理延迟没有增加的说法 W0 + BA 仅适用于合并后的单个适配器,在合并状态下混合不同的批次适配器是很困难的 |
支持。 我们的适配器占基础模型的 0.19%(3,211,264 个,根据模型配置计算得出一致)。第 3 节中 8.5~17.9% 的降幅是不进行合并、在每个请求中进行选择的方式所带来的代价 |
| 多适配器服务的吞吐量 | Chen 等人, Punica, MLSys 2024, arXiv 2310.18547, 第 7 节图 11。Sheng 等人, S-LoRA, MLSys 2024, arXiv 2311.03285, 第 7.2 节表 3 | Punica: 单张 A100 80GB,Llama-2 7B·13B,秩 16。仅使用相同适配器时,比仅主干网的 vLLM 低约 8.4% (7B) 和 12.2% (13B)(根据论文数值计算),每个 Token 增加约 2ms 延迟。S-LoRA: 将适配器保存在主机内存中,仅将运行中请求的适配器拉取到 GPU,即使适配器从 5 个增加到 100 个也能维持吞吐量,仅有微小下降 | 有限支持。 使用适配器的请求吞吐量下降且数量级相同。没有与「超出槽位时减半」相对应的外部数值,S-LoRA 解释称活跃适配器数量决定了代价,这暗示该现象可能专属于 vLLM 的 max_loras 配置 |
--max-loras 含义 |
vLLM 0.31.0 源码 config/lora.py, v1/core/sched/scheduler.py(直接从本文使用的图片中读取) |
单批次中不同的 LoRA 数量(默认 1)。达到限制时,会跳过其他适配器的请求。 max_cpu_loras 是独立的(默认与 max_loras相同) |
修正先前的表述。 上文第 4 节 |
| 开销与运行时加载 | vLLM 文档 「LoRA Adapters」(标记为 2026-09-28) | 记载称以较低开销按请求提供适配器服务,但没有给出定量数值。 VLLM_ALLOW_RUNTIME_LORA_UPDATING 警告称除非在隔离的可信环境中,否则不要在生产环境中使用,且未写明具体原因 |
有限支持。 「较低开销」在此环境中为 8.5~17.9%,并不算小 |
未确认的内容
- 训练过适配器时的速度与输出质量。 仅使用随机适配器测量了速度。
- 当模型更大、秩更高、且适配器针对所有线性层时的代价。
- 重复误差。 只有槽位测试重复了 2 次,第 3 节仅为 1 次。混合 a 和 b 的值比单独使用 a 更高的差异未作解释。
- 被推迟请求的等待时间与批次大小。 未读取服务器指标(Prometheus)。
--max-cpu-loras等 CPU 端缓存设置的效果以及数百个适配器的规模。- 存在适配器时与前缀缓存的交互(缓存是否按适配器隔离)。
参考资料
- vLLM 文档, LoRA Adapters: https://docs.vllm.ai/en/latest/features/lora/
- Hugging Face PEFT, LoRA: https://huggingface.co/docs/peft/package_reference/lora
- 本系列第 14 篇(vLLM 负载与服务器准备时间)、第 21 篇(Token 速度与内存带宽)
确认的版本与日期
截至 2026-10-07。vLLM 0.31.0, PEFT 0.21.2, accelerate 1.15.0, Qwen3-1.7B(bf16)。