文章

Multi-LoRA 适配器缓存生产实战:GPU/CPU 两级驻留、LRU 淘汰与 LoRA Affinity 降低冷加载尾延迟

面向多租户大模型推理平台,系统讲解 Multi-LoRA 适配器在 GPU/CPU 两级缓存中的驻留、LRU 淘汰、冷加载延迟与亲和路由设计,并给出容量规划、监控指标、发布治理和上线检查方法。

Multi-LoRA 适配器缓存生产实战:用 GPU/CPU 两级驻留、LRU 淘汰与 LoRA Affinity 降低冷加载尾延迟

很多团队把 Multi-LoRA 的难点想象成「如何在一个基础模型上同时跑多个低秩增量」,但真正上线多租户平台之后才会发现:矩阵计算往往不是瓶颈,适配器的生命周期管理才是。同一个推理请求,仅仅因为目标 adapter 驻留的位置不同,首 Token 延迟就可能落在完全不同的数量级上。本文面向 LLM / 生成式 AI / RAG / Agent 推理平台的工程读者,系统梳理 Multi-LoRA 适配器的两级驻留、LRU 淘汰、冷加载延迟、亲和路由与版本发布治理,并给出容量规划、监控指标与上线检查清单。

为什么 Multi-LoRA 上线后,真正难的往往不是矩阵计算

LoRA 的工程吸引力很直接:基础模型参数保持不变,每个业务或租户只保存一组低秩增量权重。相比「一个租户一份完整模型」,它显著降低了模型存储与显存重复占用,也让一个基础模型承载几十、几百甚至更多定制能力成为可能。

但当系统从「同时服务几个 LoRA」走向真正的多租户平台后,瓶颈会迅速从 LoRA 算子本身 转向 适配器生命周期管理。一个请求到来时,目标 adapter 可能处于几种完全不同的状态:

  1. 已在当前 GPU 的活跃 LoRA slot 中,几乎可以直接执行;
  2. 仍在本机 CPU 内存中,需要复制到 GPU 并完成激活;
  3. 只在本地磁盘中,需要先读取、解析、注册,再搬到 GPU;
  4. 只存在对象存储或远程模型仓库中,需要经过网络下载后再进入上述流程;
  5. 已经在另一台推理实例上处于 warm 状态,但网关把请求路由到了错误的 Pod。

因此,同样一次推理请求,仅仅因为 adapter residency 不同,首 Token 延迟就可能落在完全不同的延迟区间。Multi-LoRA 生产优化的核心问题,不是「能不能加载多个 LoRA」,而是如何让高概率被访问的 adapter 尽量靠近 GPU,同时避免缓存抖动。

核心原理:把 LoRA 当成一套分层缓存

GPU、CPU、磁盘实际上构成了 adapter 的存储层级

以 vLLM 为例,当前 LoRA 配置包含 max_lorasmax_cpu_lorasmax_lora_rank 等关键参数,它们的职责经常被误读:

参数含义常见误区
max_loras控制一个 batch 中最多可同时使用多少个 LoRA被误认为「服务器最多能支持多少个 LoRA」
max_cpu_loras控制 CPU 侧最多缓存多少个 LoRA,要求不小于 max_loras误以为「越大越好」,忽略 RSS 与 NUMA 成本
max_lora_rank决定支持的最大 rank设得远高于真实 adapter rank 会造成额外内存开销

从生产系统视角看,可以把 adapter 生命周期抽象成一条多级缓存链:

Remote Registry / Object Storage

Local Disk

CPU Adapter Cache

GPU Active Slots

Inference

这与传统 Web 系统的多级缓存非常相似,只是这里每次 miss 的成本更高:除了 I/O,还可能涉及权重解析、内存分配、CPU→GPU 数据传输和 GPU slot 激活。vLLM 的 LoRA worker manager 会在容量不足时先加载新 adapter、确认有效后再按 LRU 逻辑移除最旧 adapter,并在命中已有 adapter 时通过 touch 更新其缓存位置;TensorRT-LLM 的 LoraCache 同样明确使用 LRU eviction policy,且正在执行任务使用的 adapter 不允许被驱逐。这说明一个重要事实:adapter eviction 不是外围运维问题,而是 serving runtime 的核心状态。

冷加载延迟应该被拆成不同等级,而不是只有 hit/miss

如果监控里只有一个「LoRA load latency」,很难指导优化。更实用的分类是:

等级含义开销构成
L0GPU hit直接执行
L1CPU hitH2D upload
L2Local disk hitdeserialize + CPU cache + H2D
L3Remote registry missnetwork download + L2

真正需要关注的是各层命中比例和每一层的 P50/P95/P99 开销。首 Token 延迟可以用下面这个公式理解:

TTFT ≈ queue_wait + adapter_resolve + adapter_load + adapter_h2d + prefill + scheduler_overhead

如果团队只看总 TTFT,就会把 adapter 冷加载误认为模型计算变慢;只看 GPU utilization,也可能完全看不出问题。

Adapter 容量不能只按「个数」规划

两个 rank 不同、target module 不同的 LoRA,其权重体积可能相差很多。一个简单的近似估算是:

adapter_bytes ≈ Σ[(d_in + d_out) × rank × bytes_per_param]

求和范围是所有实际注入 LoRA 的线性层。真实占用还要考虑 tensor 对齐、padding、runtime 元数据、临时 buffer 和实现差异,因此容量规划不能只写「GPU 最多放 8 个 adapter」。更合理的方法是维护 adapter manifest:

adapter_id: claims-assistant-v17
base_model: qwen3-32b
revision: 7f83c1a
rank: 32
dtype: bfloat16
target_modules:
  - q_proj
  - k_proj
  - v_proj
  - o_proj
estimated_bytes: 612368384
priority: gold
warm_policy: gpu-preferred

这份 manifest 不只是发布清单,也应该成为调度、预热和容量控制的输入。

为什么纯 LRU 在多租户场景下容易失效

LRU 的优点是简单,而且对具有明显时间局部性的请求非常有效。但 Multi-LoRA 多租户流量很容易出现 adapter thrashing:假设一个实例只能让 4 个 adapter 保持 GPU 活跃,而某个批处理租户突然在短时间内依次访问 20 个低频 adapter,纯 LRU 会不断驱逐原本高频的在线 adapter。批处理流量结束后,在线业务的热门 adapter 又要重新加载,最终形成持续抖动。

因此,生产环境通常需要在 runtime 原生 LRU 之上增加平台策略:

  • 热 adapter 固定或半固定:对核心租户、核心业务 adapter 设置 warm set,高峰期不允许低优先级 adapter 把它们全部挤出缓存。底层 runtime 支持 pin 就用 pin,不支持就在路由层通过实例池隔离、专用 warm pool 或负载约束实现等效效果。
  • 每租户设置驻留预算:限制同一租户在一个实例上的 GPU/CPU adapter 数量,避免一个租户用大量 adapter 污染共享缓存,超出部分进入低优先级队列或专用实例。
  • 给短时间 burst 设置冷却窗口:如果一个 adapter 只被访问一次,不应该马上把刚变冷的核心 adapter 驱逐。可以增加最短 residency TTL,或对刚加载但命中次数极低的 adapter 采用更谨慎的晋升策略。
  • 采用 recency + frequency 的混合评分:LRU 只看最近访问时间,平台层可以加入频率、租户等级、SLO、加载成本等因素:
keep_score = w1 × recent_access + w2 × request_frequency + w3 × tenant_priority + w4 × reload_cost - w5 × adapter_size

这里不必追求复杂算法,关键是避免低价值 burst 轻易驱逐高价值 warm adapter。

LoRA Affinity:缓存命中率必须和路由器联动

单实例内部做再好的 adapter cache,如果入口负载均衡完全不知道哪个 Pod 已经缓存目标 adapter,仍然会浪费大量 warm 状态。Kubernetes Gateway API Inference Extension 的模型服务协议已经把 LoRA Adapter availability 纳入推理路由输入,并定义了 max_lorarunning_lora_adapterswaiting_lora_adapters 等状态,目标是让 Endpoint Picker 能对已经具备目标 LoRA 的 Pod 给予 affinity。

一个实用的路由评分可以写成:

def score(endpoint, adapter_id):
    score = 0
    if adapter_id in endpoint.gpu_resident_adapters:
        score += 100
    elif adapter_id in endpoint.cpu_resident_adapters:
        score += 60
    score -= endpoint.queue_depth * 8
    score -= endpoint.active_tokens / 1000
    score -= endpoint.recent_evictions * 3
    return score

重点不在具体系数,而在两个原则:

  1. 优先利用已经 warm 的 adapter;
  2. 亲和不能压过实时负载。

如果某个 Pod 虽然命中了 adapter,但排队已经严重,继续把请求粘过去会让 TTFT 更差。因此真正有效的是 affinity-until-saturated:在实例未达到负载阈值前利用亲和,一旦饱和就回到负载优先。

值得注意的是,vLLM 在 2026 年的一个公开 feature request 中专门讨论了「LoRA Adapter Cache Residency」可观测性:现有 vllm:lora_requests_info 更偏向运行中/等待中的 adapter 状态,不能完整区分「GPU 已缓存但空闲」「CPU 已缓存」「从未加载」。这目前是一个正在讨论的观测缺口,不是已稳定发布的能力。对生产团队来说,这意味着不能假设 runtime 已经暴露了所有所需指标,必要时要在 adapter manager、router 或 sidecar 层补齐 telemetry。

工程落地:把 adapter 管理拆成控制面和数据面

控制面:管理「哪个 adapter 应该被加载」

控制面建议至少维护以下信息:

  • adapter logical name;
  • immutable revision 或 digest;
  • base model identity;
  • LoRA rank、dtype、target modules;
  • 文件大小与校验和;
  • tenant / namespace;
  • 发布状态:canary、stable、deprecated;
  • warm policy:GPU preferred、CPU only、on demand;
  • rollback revision。

不要让在线请求直接携带任意本地路径或任意远程 URL 去触发动态加载。vLLM 官方安全文档明确提示:动态 LoRA loading 存在安全风险,不应直接暴露给不可信客户端。生产系统应通过受控 registry 和管理接口完成发布,由网关只允许请求已登记的 logical model name。

数据面:管理「adapter 当前在哪里」

每个推理 worker 应维护可观测的 residency 状态:

adapter_id  revision  tier = gpu | cpu | disk | absent
last_access_time  load_count  hit_count  eviction_count
bytes  pinned  inflight_requests

当一个 adapter 正在被请求使用时,必须避免被 eviction——TensorRT-LLM 的 LoraCache 就明确把 in-progress task 保护在淘汰之外。

路由层:让 residency 成为调度信号

推荐至少有三个分值:adapter affinity score、endpoint load score、tenant/SLO priority score。不要把 Multi-LoRA 调度退化成普通 round-robin 或 least-connections——LLM 请求成本差异很大,而且 adapter 冷加载具有明显状态依赖。

预热策略:不是「启动时全部加载」

很多团队上线第一版 Multi-LoRA 时,会把所有 adapter 都配置为启动预加载。adapter 数量少时没问题,一旦进入数百甚至上千规模,这种做法就不可持续。更合理的是三层 warm policy:

Tier策略适用对象
A:GPU 常驻占用 GPU slot最高频、最严格 SLO 的 adapter,数量必须非常有限
B:CPU 常驻只做 H2D有稳定流量、但不值得占用 GPU slot 的 adapter
C:按需加载只保留 artifact cache / registry 引用长尾 adapter,请求到达时加载

warm set 不能靠人工长期维护。可以基于 5 分钟、1 小时、24 小时三个时间窗口滚动统计,将 adapter 自动升降级,但发布切换期间要允许运维显式 pin 新版本。

Adapter 版本发布:缓存命中不能牺牲版本正确性

缓存系统里最危险的问题不是 miss,而是命中了错误版本。因此 adapter cache key 不应只有 tenant + adapter_name,而应至少包含:

base_model_digest + adapter_name + adapter_revision + adapter_digest + runtime_compatibility_version

假设 customer-service adapter 从 v17 发布到 v18,如果缓存只按 logical name 命中,就可能出现「路由认为 Pod 是 warm 的,但实际 resident 的仍然是 v17」。推荐的发布流程是:

artifact upload → checksum verify → compatibility validation
→ CPU prewarm on canary pool → GPU activate on selected pods
→ health probe → small traffic canary → promote stable routing
→ keep old revision for rollback window → delayed eviction

如果使用 Kubernetes InferenceModel 一类抽象,还可以在 logical model 到 LoRA revision 的映射层做渐进发布和流量切分,而不是直接修改每个业务调用方。

需要重点监控哪些指标

生产环境至少应该区分以下四类指标。

1. Residency 与命中

adapter_cache_hits_total{tier="gpu"}
adapter_cache_hits_total{tier="cpu"}
adapter_cache_misses_total
adapter_resident_count{tier="gpu"}
adapter_resident_count{tier="cpu"}

如果 runtime 没有原生提供,可以在平台层采集。为避免 Prometheus label cardinality 爆炸,不建议默认把所有 adapter_id 都做长期标签;保留租户级聚合,并对 top-K 热 adapter 单独观测。

2. 加载与搬运

adapter_resolve_seconds
adapter_disk_load_seconds
adapter_remote_download_seconds
adapter_h2d_seconds
adapter_activation_seconds

这些指标能直接定位 TTFT 突刺发生在哪一层。

3. Eviction 与抖动

adapter_evictions_total{tier="gpu"}
adapter_evictions_total{tier="cpu"}
adapter_reload_after_eviction_total
adapter_thrash_ratio

thrash_ratio 可以定义成「被 eviction 后在短时间窗口内再次加载的 adapter 次数 / 总 eviction 次数」。这个值持续升高,通常说明缓存容量、租户隔离或 eviction policy 有问题。

4. 请求体验

至少按 warm/cold 分类观察 TTFT P50/P95/P99TPOT P50/P95/P99queue_waitrequest_error_ratecold_start_rate。如果冷请求 TTFT 高但 warm 请求稳定,首先应该优化 adapter residency,而不是直接扩 GPU。

常见误区

  1. max_loras 就是「服务器最多能支持多少个 LoRA」——不是。它约束的是单 batch 可同时使用的 LoRA 数量,并影响活跃 LoRA 相关内存。一个实例可以通过 CPU cache 或动态加载管理更多 adapter,但并不意味着它们都能同时驻留 GPU。
  2. CPU cache 越大越好——CPU cache 过小会导致频繁磁盘/远端 miss,但无限增大同样可能造成 RSS 持续膨胀、NUMA 访问成本上升和宿主机压力。这类行为必须针对具体 runtime 版本压测,而不能假设 /unload 后 RSS 会立刻下降。
  3. 只要有 LoRA affinity,就应该始终粘住命中实例——错误。命中收益必须和 queue depth、active tokens、并发流数量一起看。已经饱和的 warm Pod 可能比未命中的空闲 Pod 更慢。
  4. 把动态 LoRA 加载接口直接暴露给调用方——风险很高。动态加载意味着服务器可能读取本地或远程模型资源,属于高权限控制面操作,官方文档明确建议只在受信环境中开放,并通过反向代理或网络控制限制访问。
  5. 只看平均加载时间——adapter 冷加载是典型长尾问题,平均值可能很好看,但少量 L3 remote miss 就足以把 P99 TTFT 拉高,必须按 tier 和 percentile 拆开。

上线检查清单

在生产发布 Multi-LoRA 服务前,建议逐项确认:

  • 基础模型与 adapter revision 都有不可变 digest;
  • adapter rank、dtype、target modules 在注册时完成兼容性校验;
  • max_lorasmax_cpu_lorasmax_lora_rank 已按真实 adapter 分布压测;
  • 已区分 GPU hit、CPU hit、disk hit、remote miss;
  • eviction 不会驱逐正在执行请求使用的 adapter;
  • 核心 adapter 有明确 warm/pin 策略;
  • 单租户不能无限污染共享 adapter cache;
  • router 能感知 LoRA affinity,同时考虑 queue/load;
  • adapter cache key 含 base model 与 revision 信息;
  • 新版本支持 canary、预热、回滚;
  • 动态 load/unload 管理接口没有暴露给普通外部调用方;
  • cold/warm TTFT 分开监控;
  • GPU/CPU adapter eviction 和 reload-after-eviction 已纳入告警;
  • 压测包含「高频 adapter + 长尾 burst」场景,而不是只测均匀流量。

适用场景与边界

这套设计最适合以下工作负载:SaaS 平台为多个租户提供专属微调模型;一个基础模型上挂载大量领域 adapter;企业内部按部门、产品线或业务流程划分 LoRA;adapter 数量远大于单卡可同时驻留数量;对 P95/P99 TTFT 有明确 SLO;需要频繁发布 LoRA 新版本但不希望复制整套基础模型服务。

如果只有 2~3 个长期固定 adapter 并且显存充足,复杂的 cache-aware routing 未必有收益,直接预加载反而更简单。只有当 adapter 数量、租户数和流量动态性开始扩大时,两级缓存和亲和调度的价值才会明显出现。

参考资料

  1. vLLM LoRA Adapters 官方文档:https://docs.vllm.ai/en/latest/features/lora/
  2. vLLM LoRAConfig 官方 API 文档:https://docs.vllm.ai/en/latest/api/vllm/config/index.html
  3. vLLM LoRA worker manager 源码:https://github.com/vllm-project/vllm/blob/main/vllm/lora/worker_manager.py
  4. TensorRT-LLM LoraCache Runtime 文档:https://nvidia.github.io/TensorRT-LLM/1.2.0/_cpp_gen/runtime.html
  5. S-LoRA: Serving Thousands of Concurrent LoRA Adapters:https://arxiv.org/abs/2311.03285
  6. Punica: Multi-Tenant LoRA Serving:https://arxiv.org/abs/2310.18547
  7. Kubernetes Gateway API Inference Extension: Model Server Protocol / LoRA Adapter Serving:https://github.com/kubernetes-sigs/gateway-api-inference-extension/blob/main/docs/proposals/003-model-server-protocol/README.md
  8. vLLM Feature Request: Expose LoRA Adapter Cache Residency in Metrics(用于说明当前观测缺口,不视为已稳定发布能力):https://github.com/vllm-project/vllm/issues/45325

常见问题

max_loras 越大,Multi-LoRA 服务能力就一定越强吗?
不一定。max_loras 主要约束单批次可同时使用的 LoRA 数量,并会影响预分配显存。过度放大可能浪费显存,生产环境应结合真实并发适配器数、rank 分布和命中率压测后确定。
为什么已经共享了基础模型,仍然会出现明显的长尾延迟?
共享基座只解决了基础权重重复加载的问题。目标适配器若不在 GPU 或 CPU 缓存中,请求仍需经历磁盘或远程读取、反序列化、CPU 驻留、Host-to-Device 传输和激活,冷加载会直接抬高 TTFT。
LoRA Affinity 是否应该永远优先于负载均衡?
不应该。亲和路由的价值是复用已驻留的适配器,但当目标实例排队过深或接近容量上限时,继续粘住同一实例可能放大尾延迟,生产路由应在命中收益与实时负载之间权衡。
动态 LoRA 加载接口可以直接开放给调用方吗?
风险很高。动态加载意味着服务器可能读取本地或远程模型资源,属于高权限控制面操作,官方建议仅在受信环境开放,并通过反向代理或网络控制限制访问。