LoRA 热加载与 adapter 路由#
LoRA(Low-Rank Adaptation)是当前主流的模型微调方法之一,它不修改基础模型权重,而是通过插入小型的低秩矩阵来实现对特定任务的适配。SGLang 支持在运行时动态加载 LoRA adapter、在不同请求间切换 adapter,以及同时维护多个 adapter 的活跃状态。
这一节回答三件事:
- LoRA 的权重结构是什么,以及它在推理时如何工作;
- SGLang 怎样在单次 forward pass 中同时处理多个使用不同 adapter 的请求;
- LoRA adapter 的生命周期(加载、路由、卸载)在代码里落在哪里。
LoRA 的权重结构#
LoRA 不修改原有参数,而是为特定层(通常是 attention 的 Q、K、V、O projection 和 MLP 的 gate/up/down projection)添加两个小矩阵:
W_adapted = W_base + A × B
其中:
- W_base: [d_out, d_in],冻结不变
- A: [d_out, r],低秩矩阵,r << d_out
- B: [r, d_in],低秩矩阵
- A × B: [d_out, d_in],和 W_base 维度相同r 是 rank(秩),通常取 8、16 或 32。当 r=16,d_in=d_out=4096 时,每层 Q projection 的参数量从 4096×4096 = 16M 降到 4096×16 + 16×4096 = 131K,压缩了约 122 倍。
推理时不需要提前把 A×B 加到 W_base 上,而是通过分离计算实现:
output = W_base × input + scale × (A × (B × input))
≈ base_output + lora_output这样 W_base 可以跨所有 adapter 共享,只有 A 和 B 需要按 adapter 各自存储。
多 adapter 的 Batch 执行#
SGLang 最重要的 LoRA 特性是:同一个 batch 里可以包含使用不同 adapter 的请求,而不需要把 batch 按 adapter 分拆。
朴素做法的问题:如果每个请求用的 adapter 不同,最简单的做法是把 batch 按 adapter 分组,每组单独跑。但这会把 batch 切碎,GPU 利用率下降。
SGLang 的做法:batched LoRA 计算
base_output = W_base @ X_batch # [batch, seq, d_out],所有请求共享
# 对每个 adapter,找到使用它的请求子集
for adapter_id, req_indices in batch.lora_groups.items():
X_sub = X_batch[req_indices] # [n_sub, seq, d_in]
lora_out = A[adapter_id] @ (B[adapter_id] @ X_sub)
base_output[req_indices] += scale * lora_out这样 W_base 的矩阵乘法仍然是全 batch 一次,LoRA 部分是按 adapter 分组的小矩阵乘法,总体上保持了高 GPU 利用率。
adapter 在 Req 中的字段#
当请求指定 lora_path 时,它会被 TokenizerManager 在请求进入 scheduler 之前解析成一个 lora_id:
if obj.lora_path:
lora_id = self.lora_manager.get_or_register_adapter(obj.lora_path)
else:
lora_id = None # 使用基础模型Req 对象里有 lora_id 字段,Scheduler 在组 batch 时会把各请求的 lora_id 收集到 ScheduleBatch。到了 ForwardBatch 层,这些信息变成一个 per-request 的 adapter 索引数组。
Adapter 路由:从请求到 forward#
在 ForwardBatch 里,LoRA 信息以这种形式存在:
lora_batch_info = LoraBatchInfo(
lora_ids=[...], # 每个请求对应的 adapter id,None 表示不用 LoRA
lora_a_strides=[...], # 各 adapter 的 A 矩阵在内存里的位置
lora_b_strides=[...], # 各 adapter 的 B 矩阵在内存里的位置
)ModelRunner.forward() 把这个对象传给模型,模型在每个 LoRA 层里读取当前请求对应的 adapter 权重并执行 lora_output 的计算。
Adapter 的生命周期#
加载(Load)
adapter 在第一次被请求使用时加载。加载过程:
- 读取 adapter 的
.safetensors文件(或从 HuggingFace Hub 拉取) - 验证 adapter 的 rank 和目标层与当前模型兼容
- 把 A、B 矩阵加载到 GPU 显存,注册到 LoRA 管理器
显存占用:每个 adapter 仅需存 A、B 矩阵。rank=16 的 Llama-3 8B 的完整 LoRA adapter 约 30–60 MB,远小于基础模型的 16 GB。
并发维护多个 adapter
SGLang 允许同时在 GPU 显存里保留多个 adapter。通过 --max-loras-per-batch 控制单次 batch 里能同时出现的 adapter 数量,通过 --max-lora-rank 控制能接受的最大 rank。
卸载(Eviction)
如果加载的 adapter 数量超过预设上限,SGLang 会按 LRU 策略卸载最久未使用的 adapter。被卸载的 adapter 下次再被请求时需要重新从磁盘加载。
调试 LoRA 时先看哪里#
现象:指定了 lora_path 但输出和基础模型一样
- 确认请求里的
lora_path是否被正确解析成了lora_id; - 检查
ForwardBatch.lora_batch_info里这个请求的lora_id是否为 None; - 确认 adapter 文件路径是否正确,adapter 是否成功加载到 GPU。
现象:LoRA 请求比普通请求慢很多
- 检查
--max-loras-per-batch是否设置过小(频繁切换导致 batch 分组过细); - 检查 adapter 的 rank 是否过大(rank=128 的开销是 rank=16 的 8 倍);
- 用
/metrics检查是否频繁触发 adapter eviction(磁盘加载很慢)。
现象:多个不同 adapter 的请求在同一 batch 里但输出都像同一个 adapter
这说明 LoraBatchInfo 的 per-request adapter 路由可能出现了问题。检查 ScheduleBatch 里各请求的 lora_id 是否被正确传递到 ForwardBatch。
小结#
- LoRA 通过 A×B 低秩矩阵在推理时插入,不修改基础模型权重;
- SGLang 用 batched LoRA 计算让同一 batch 里的不同 adapter 共享 W_base 的矩阵乘法;
- adapter 以
lora_id的形式从Req传递到ForwardBatch,在 forward 阶段被 per-request 地应用; - adapter 的生命周期由 LoRA 管理器控制,支持 LRU 卸载和多 adapter 并发。
理解了这层机制,再看多 adapter 场景下的性能分析和调试,就不会把 LoRA 开销误归到基础模型本身。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。