后台化妆间里,一位演员正对镜上妆。他演了一辈子戏,身段、唱腔、功底都是现成的——可每次登台都得换一副新面孔:演将军就描剑眉,演书生就点朱唇,演农夫就抹上风霜色。面具薄如蝉翼,盖上去,观众看到的是另一个人;摘下来,演员还是那个演员。 大模型的世界里也有这样的“演员”。辛苦训练出来的基础大模型,内功深厚、样样通吃,可要它同时干好写诗、客服、翻译、写代码这些活,就会顾此失彼。于是工程师发明了 LoRA——一种轻得不能再轻的“面具”:不碰模型内部一根筋骨,只在关键部位贴几片薄薄的“妆容”。同一个模型,戴上这副面具是诗人,换上那副就是客服。而真正考验功夫的,是如何在几十上百副面具之间瞬间换脸、不动声色。这就是多 LoRA 切换要解决的事。

岗位关联

多 LoRA 切换是 AI 部署工程师的核心技能:它让一个模型靠“换面孔”服务几十上百个任务,把 GPU 利用率提升数倍,是大模型商业化落地不可或缺的技术拼图。

目录
  1. 可视化演示
  2. LoRA基础认知:认识那副“薄面具”
  3. 切换核心机制:换脸只需一个呼吸
  4. 路由调度策略:检票员的手腕
  5. 显存优化管理:面具柜的收纳哲学
  6. 并发推理优化:群演同台,一人一张脸
  7. 部署工程实践:后台化装间的搭建
  8. 多租户场景:一个剧场,百家戏班
  9. 性能与挑战:换脸的速度与代价
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示多LoRA切换的核心机制,通过开始/执行/监控/完成四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

多LoRA切换:LoRA LoRA热切换原理与实现机制 动态加载与GPU显存管理策略 核心机制可视化 多LoRA切换整体架构与关键组件

LoRA基础认知:认识那副“薄面具”

想象化妆台上一排面具,每一副都薄如蝉翼。演员要演侠客,化妆师只给他戴上一副剑眉星目的面具,绝不会给他换一身几十斤的铠甲。为什么?因为换身体等于换演员,代价太大;而换面具,代价几乎为零。LoRA 的故事,就从这“薄”字开头。

LoRA 的全称是低秩适配(Low-Rank Adaptation),它看穿了一件事:模型学习一个新任务时,并不需要把整个身体的参数都翻新,真正的变化往往浓缩在一个极小的“低秩空间”里。于是它只给若干层并联两个小矩阵,用它们的乘积来近似“模型应该变多少”。

设某层的原始权重为 $W_0$(一个 d×k 的矩阵),LoRA 学到的增量写作 $\Delta W = BA$,其中 B 的形状是 d×r,A 的形状是 r×k。r 就是“秩”,一个巴掌大的数字,通常取 8~64。推理时的计算依然是一个矩阵乘,只是把增量叠加上去:

面具轻,后台就能多备几副。LoRA 之所以撑得起“多副面孔”的野心,正因为它薄、它小、它便宜——几十 MB 的妆容,换来一种全新的身份。

h=(W0+BA)xh = (W_0 + BA)x
这个式子的意思是:LoRA 不是另起炉灶,而是在原配方上“加一勺调料”。
  1. 原始输出 $W_0 x$$x$ 是这一层的输入向量,$W_0$ 是冻结不动的原始权重,两者相乘得到“素颜”状态下该层的输出——演员本色出演的原始反应。
    W0xW_0 x
  2. 低秩增量 $BA$$B$(d×r)和 $A$(r×k)两个小矩阵相乘,还原出一个与 $W_0$ 同形状的增量矩阵。因为 $r$ 远小于 d、k,这个增量的“有效自由度”被锁在一个极小的秩里。
    BABA
  3. 叠加输出 $(W_0 + BA)x$先加后乘,等价于先“戴上面具”再对输入做反应。这副面具的参数量可能只有全量微调的万分之一,占用的显存常常只有几十 MB。
    (W0+BA)x(W_0 + BA)x

切换核心机制:换脸只需一个呼吸

后台换装讲究一个“快”字。老练的化妆师能在演员喝口水、擦把汗的间隙,把第二副面具严丝合缝地贴上去。模型服务也一样,理想状态下换 LoRA 应该像呼吸一样自然,进程不用停、请求不用断。

这个动作叫 LoRA 热切换。基础模型权重本身不动,换的只是那层“妆容”。实现上有两种典型姿势:第一种是权重合并,把增量直接“烙”进原始权重,之后前向计算彻底变成 $(W_0+BA)x$,速度快,但每次换脸要把权重矩阵改写一遍;第二种是分离推理,低秩分支单独存放,前向时分开算 $h = W_0x + BAx$,切换时只换那对薄薄的小矩阵,连大权重都不碰。

权重合并的数学就一步:

分离推理像“面具挂在脸上但不粘死”,换人只需摘下来换一副;合并推理像“面具直接印在皮肤上”,换的时候要重新画一遍。两种姿势各有用武之地,但都保证了一件事:演员不用下台。

Wmerged=W0+α⋅(B⋅A)W_{merged} = W_0 + \alpha \cdot (B \cdot A)
合并之后,模型推理的速度与未挂 LoRA 时几乎完全一样,因为权重变成了一张完整的“新面孔”。
  1. 矩阵相乘 $B \cdot A$先由两个低秩小矩阵还原出完整的增量矩阵,这一步相当于把化妆师手里的“妆容配方”铺展开来。
    B⋅AB \cdot A
  2. 缩放系数 $\alpha \cdot (\cdot)$$\alpha$ 控制这副面具的“浓度”。取值太小等于没化妆,太大则五官夸张,实践中常在 1~2 之间。
    α⋅(B⋅A)\alpha \cdot (B \cdot A)
  3. 合并回原始权重 $W_0 + \cdots$把增量矩阵逐元素加到 $W_0$ 上,合并完成。之后前向计算与普通模型别无二致,延迟零增加。
    W0+α⋅(B⋅A)W_0 + \alpha \cdot (B \cdot A)

路由调度策略:检票员的手腕

剧场门口人来人往,有人攥着武生戏的票,有人要看去文戏。检票员不用多问,只看一眼票根的颜色,手一抬,就把人引向对应的门。在多 LoRA 系统里,这位检票员叫路由调度器,它决定每一个请求该配哪一副面具。

最朴素的路由是标签匹配:请求头上带一个显式的标识,比如租户 ID、任务类型、渠道来源,调度器拿它查一张“领号码→LoRA”的映射表。更聪明的是语义路由:把请求的文本编码成向量,拿它和每副 LoRA 的“简介向量”算相似度,谁最像就让谁上场——相当于检票员看观众的言谈举止,猜他更想看哪种戏。

语义路由的决策可以写成:

其中 $q$ 是请求文本的向量表示,$d_l$ 是第 $l$ 个 LoRA 的简介向量,sim 常用余弦相似度。一句话:把请求送给“看着最顺眼”的那副面具。

检票员手上那本剧目表,就是生产环境里的路由表。标签匹配像“认票根”,语义路由像“察言观色”,目的都一样:让每个请求在最短时间内,站到属于它的那扇门前。

lsel=arg⁡max⁡l∈L  sim⁡(q,dl)l_{sel} = \arg\max_{l \in L} \; \operatorname{sim}(q, d_l)
  1. 请求向量 $q$用一个嵌入模型把用户输入(比如“帮我写几句广告语”)编码成一串数字,这串数字浓缩了请求的语义。
    q=Embed(input)q = \mathrm{Embed}(input)
  2. 相似度 $\operatorname{sim}(q, d_l)$逐个计算 $q$ 和每副面具简介向量之间的余弦相似度,衡量“这单活和哪种戏路最投缘”。
    sim⁡(q,dl)=q⋅dl∥q∥∥dl∥\operatorname{sim}(q, d_l) = \frac{q \cdot d_l}{\|q\| \|d_l\|}
  3. 挑选 $\arg\max$在所有 LoRA 中挑相似度最高的那个编号 $l$,它就是本次推理解锁的面具。
    lsel=arg⁡max⁡l  sim⁡(q,dl)l_{sel} = \arg\max_{l} \; \operatorname{sim}(q, d_l)

显存优化管理:面具柜的收纳哲学

化妆间再大,也架不住把一百副盔甲全摊在地上。GPU 显存比化妆间还金贵:一个 7B 参数的基础模型通常要占十几个 GB,要是每副 LoRA 都赖着不走,几十副下来,再大的显存也要爆仓。

所以显存管理的第一原则是“少驻留、勤搬运”。基础模型权重永远常驻;最热门的几副 LoRA(比如每天都有人点的爆款脸)也常驻;冷门的 LoRA 则躺在 CPU 内存甚至磁盘里,用到时再搬上显存,用完立刻请走——这和化妆师把常用面具挂手边、冷门盔甲锁仓库是一个道理。具体策略包括权重合并后立刻释放 LoRA 占用的缓冲、预留交换区让冷门面具“用完即弃”,以及用 INT8 量化给面具进一步瘦身。

显存预算大致分成三块:

收纳得当,一百副面具也能在巴掌大的化妆台上有条不紊。显存优化的本质,就是把“最常用的留在手边,最冷门的送到仓库,周转位永远只留给正在化妆的那一位”。

Mtotal=Mbase+Mhot+MswapM_{total} = M_{base} + M_{hot} + M_{swap}
把显存切成“常驻区”和“周转区”,心里有数,才不会爆显存。
  1. 基础占用 $M_{base}$基础模型权重的显存,永远在卡上,可以通过 INT8 量化为它瘦身。
    Mbase=size(W0)M_{base} = \mathrm{size}(W_0)
  2. 热区占用 $M_{hot}$常驻的高频 LoRA 占用的显存,通常只留最热的 3~5 副,这是“手边常备的面具”。
    Mhot=∑l∈hotsize(l)M_{hot} = \sum_{l \in \text{hot}} \mathrm{size}(l)
  3. 交换区 $M_{swap}$给按需搬运预留的缓冲区,冷门面具先落进这块“临时化妆台”,推理完马上清空,让下一个面具复用。
    Mswap≈max⁡lsize(l)M_{swap} \approx \max_{l} \mathrm{size}(l)

并发推理优化:群演同台,一人一张脸

演出旺季,几十位观众同时点戏,难道要开几十个舞台?聪明的剧场不是给每个观众配一个舞台,而是让所有人坐在同一片舞台前,演员按各人的点单轮番换面具。这个思路放到模型服务里,叫混批推理。

基础模型的前向计算——尤其是注意力那部分——跟 LoRA 是谁无关。几十个请求带着各自的“点单号”(LoRA 标识)拼进同一个 batch,共享一次基础模型的前向计算;等到了 LoRA 介入的层,再按各自的标识分叉到不同的低秩分支上做增量计算。这样一来,基础模型的计算只花一份钱,几十个请求一起买单,典型系统(如 S-LoRA)吞吐能翻几倍。批量与吞吐的关系大致为:

工程上还有“连续批处理”(continuous batching):不等一个 batch 全部结束,谁答完谁就退出,新请求立刻补位,像流水线一样不让 GPU 闲下来。观众越多,共享舞台布景的省法越明显——大模型只演一遍武打,座下每个人看到自己点的表情。

Throughput=B×NreqTtotal\text{Throughput} = \frac{B \times N_{req}}{T_{total}}
B 越大、等待越少,每秒喂给观众的好戏就越多。
  1. 批请求总数 $B \times N_{req}$每个 batch 里塞了 B 个批次、每批 $N_{req}$ 个请求,乘积就是这一轮总共处理的请求数。
    B×NreqB \times N_{req}
  2. 总耗时 $T_{total}$从这批请求进门到全部答完出门的总时间,包含排队、基础前向和各 LoRA 分叉的花销。
    Ttotal=Tqueue+Tbase+TloraT_{total} = T_{queue} + T_{base} + T_{lora}
  3. 吞吐量 $\text{Throughput}$一秒钟能消化多少请求。混批把 $T_{base}$ 摊薄到每个请求头上,这是吞吐提升的根本原因。
    Throughput=B×NreqTtotal\text{Throughput} = \frac{B \times N_{req}}{T_{total}}

部署工程实践:后台化装间的搭建

一个成熟的后台,不能靠化妆师手忙脚乱地翻抽屉,得有一个井井有条的化装间:面具放在哪、谁负责取、取来用多久、用旧了怎么换新,都得有规矩。多 LoRA 部署工程,就是给模型服务搭这样一间化装间。

整体架构通常是三层:最外面是网关(负责路由),中间是推理引擎(vLLM、SGLang 等,维护 LoRA 的加载与驱逐),最底下是 LoRA 仓库(管面具的注册与版本)。推理请求通过 HTTP 头或请求体带上 LoRA 标识,网关解析后调度;推理引擎维护一个“当前已加载列表”,按 LRU 策略腾挪显存。LoRA 的生命周期包括上传、校验、注册、下发、影子验证、全量上线与下线,版本升级时先小流量跑通再放量。

上线运维要盯三件事:切换延迟、加载命中率和显存水位。其中加载命中率是关键指标:

化装间上了规矩,台上换脸才不慌乱。工程能力强的团队,甚至能在监控大屏上实时看到“哪副面具最抢手、哪副在仓库吃灰”,把资源编排变成一门数据驱动的艺术。

Hitrate=NhitNtotalHit_{rate} = \frac{N_{hit}}{N_{total}}
命中率越高,说明大多数请求都撞上了“已经戴好的面具”,切换自然丝滑。
  1. 命中次数 $N_{hit}$请求到达时,目标 LoRA 已经在显存里、无需搬运的次数,等于“化妆师一伸手就够到面具”的次数。
    Nhit=∑req1[lora ready]N_{hit} = \sum_{req} \mathbb{1}[\text{lora ready}]
  2. 总请求数 $N_{total}$统计周期内所有推理请求的总数。
    NtotalN_{total}
  3. 命中率 $Hit_{rate}$命中占总请求的百分比。上线新 LoRA 时先小流量影子验证,再逐步放量,也是在保护这条曲线。
    Hitrate=NhitNtotalHit_{rate} = \frac{N_{hit}}{N_{total}}

多租户场景:一个剧场,百家戏班

剧场把舞台租给几十个戏班,每个戏班都有自己的行头箱,甲班的箱子乙班不能碰。在多 LoRA 世界里,这就是多租户:一个基础模型副本,同时服务 N 个客户,每个客户只看得见自己的 LoRA。

多租户要过三道关。第一道是映射:路由表里存着 tenant_id → lora_id 的关系,请求一到,先认租户再认面具。第二道是配额:每个租户能同时占用多少显存、多少并发,要有硬上限,防止一个戏班包场。第三道是安全边界:尤其在混批推理时,batch 里不同租户的请求必须各自携带 LoRA 标识,推理引擎按标识分别路由到对应的低秩分支,绝不能串戏。

容量规划上,租户平均资源大致为:

戏班愿意把传家宝行头寄存在你的剧场,前提是剧场管得住“各用各的、互不翻看”。多租户 LoRA 的承诺,说到底靠映射、配额、隔离三道锁一起守住。

Rt=QtotalNactiveR_t = \frac{Q_{total}}{N_{active}}
活跃租户越多,每一家分到的“舞台时间”就越少,超了就得排队。
  1. 总配额 $Q_{total}$整个推理集群可分配的显存或并发槽位总量,相当于剧场全部的舞台和化妆间。
    QtotalQ_{total}
  2. 活跃租户数 $N_{active}$当前正在产生请求的租户数量,相当于今晚实际上台的戏班数。
    NactiveN_{active}
  3. 均摊资源 $R_t$每家租户平均能拿到的资源,据此可以决定是否对新请求限流或扩容。
    Rt=QtotalNactiveR_t = \frac{Q_{total}}{N_{active}}

性能与挑战:换脸的速度与代价

演出总有换场的时候,再麻利的化妆师,摘下旧面具、擦净、贴上新面具,也得要几秒。多 LoRA 系统最大的痛点,正是这段“换脸时间”——专业点叫切换延迟。

切换延迟主要来自两个环节:一是把冷门 LoRA 从磁盘或 CPU 内存搬进显存,上百 MB 的权重走一遍 PCIe 总线,几十到上百毫秒就没了;二是换脸可能导致 KV Cache 失效——不同 LoRA 对同一段文本的注意力分布不同,旧缓存不能混用,首次请求得重新计算前缀。加起来:

对策也明确:热门 LoRA 预加载(把爆款面具提前挂到手边)、流水线传输(边搬运边开始能算的部分)、KV Cache 按 LoRA 分区(不同脸的 cache 各存各的)、多 GPU 分摊(每张卡固定管几副面具)。这些手段组合起来,能让切换延迟从百毫秒级压到几十毫秒级。换场时间短到观众聊几句天就忘了,才是好剧场——多 LoRA 切换的工程追求,就是在面具挂到几百副时,依然让每一张脸都随叫随到。

Tswitch=Tload+TrebuildT_{switch} = T_{load} + T_{rebuild}
切换快不快,就看这“搬面具”和“重新热身”两段加起来有多短。
  1. 加载耗时 $T_{load}$LoRA 权重从存储介质搬到显存的时间,取决于文件大小和 PCIe/NVLink 带宽,量化压缩能把这副面具压得更小。
    Tload=size(lora)bwT_{load} = \frac{\mathrm{size}(lora)}{bw}
  2. 缓存重建 $T_{rebuild}$KV Cache 按 LoRA 维度分区缓存、切换后清空重建的时间,等于“换脸后重新暖场”。
    Trebuild=TprefillT_{rebuild} = T_{prefill}
  3. 总切换延迟 $T_{switch}$两者的和,是用户体验上最直观的“卡顿感”来源。
    Tswitch=Tload+TrebuildT_{switch} = T_{load} + T_{rebuild}

AI 应用场景

场景1:多租户SaaS客服平台

阶段:模型部署与推理优化

解决的问题:传统做法是为每个客户单独部署一个微调模型,几十个客户就要几十套 GPU,成本爆炸。采用多 LoRA 切换后,所有客户共享一个基础模型,后台按 tenant_id 路由到各自 LoRA,显存和 GPU 数量下降一个量级——就像剧场把舞台租给几十个戏班,而不是给每个戏班盖一座剧院。

具体案例:一家做智能客服的 SaaS 厂商,把销售、售后、金融、教育等客户的 FAQ 分别微调成 LoRA,部署在同一个推理集群上。用户发消息时网关根据租户标识自动选择对应 LoRA,切换延迟控制在几十毫秒内,客户毫无感知。

场景2:代码助手的“风格面具”

阶段:推理服务架构

解决的问题:一个代码生成助手要同时讨好多种口味:有人要简洁的 Python,有人要带注释的 Rust,有人要防御式编程。单独部署多个模型太浪费,把每种风格做成一个 LoRA,按用户偏好路由。

具体案例:某 AI 编程插件在请求体里带上用户选择的“风格 ID”,网关将其映射到对应的代码风格 LoRA。产品上线后,冷门风格请求即使量小,也能共享基础模型的热度,GPU 总体利用率提升了约 3 倍。

Python 演示

python86 行
import numpy as np  class Lora:    """一副 LoRA 面具:低秩增量 B@A,附带描述向量与热度计数。"""     def __init__(self, name, d=64, k=64, rank=4, desc_vec=None):        self.name = name        self.rank = rank        # 低秩分解:B(d x r) 与 A(r x k)        self.B = np.random.randn(d, rank).astype(np.float32) * 0.1        self.A = np.random.randn(rank, k).astype(np.float32) * 0.1        if desc_vec is None:            self.desc_vec = np.random.randn(k).astype(np.float32)        else:            self.desc_vec = desc_vec        self.hit_count = 0  class LoraRouter:    """路由调度器:按请求描述向量找最近邻 LoRA。"""     def __init__(self, loras):        self.loras = loras     def route(self, query_vec):        best, best_sim = None, -2.0        for lora in self.loras:            v = query_vec / (np.linalg.norm(query_vec) + 1e-8)            w = lora.desc_vec / (np.linalg.norm(lora.desc_vec) + 1e-8)            sim = float(np.dot(v, w))            if sim > best_sim:                best, best_sim = lora, sim        best.hit_count += 1        return best  class LoraManager:    """服务端:管理显存中常驻 LoRA,按需加载与 LRU 驱逐。"""     def __init__(self, capacity=3):        self.capacity = capacity        self.loaded = {}        self.order = []     def load(self, lora):        if lora.name in self.loaded:            self.order.remove(lora.name)            self.order.append(lora.name)            return False        if len(self.loaded) >= self.capacity:            victim = self.order.pop(0)            del self.loaded[victim]        self.loaded[lora.name] = lora        self.order.append(lora.name)        return True     def infer(self, x, lora):        """等价于 h = (W0 + BA)x 的增量部分,这里只模拟 LoRA 分支。"""        return lora.B @ (lora.A @ x)  def main():    loras = [        Lora("客服", 64, 64, 4, np.random.randn(64)),        Lora("翻译", 64, 64, 4, np.random.randn(64)),        Lora("代码", 64, 64, 4, np.random.randn(64)),        Lora("文案", 64, 64, 4, np.random.randn(64)),    ]    router = LoraRouter(loras)    manager = LoraManager(capacity=3)    rng = np.random.default_rng(0)     for i in range(8):        q = rng.normal(size=64) + loras[i % len(loras)].desc_vec * 0.8        chosen = router.route(q)        swap = manager.load(chosen)        x = rng.normal(size=64)        out = manager.infer(x, chosen)        print(f"请求{i}: 路由到 {chosen.name}, 换脸={swap}, 输出范数={np.linalg.norm(out):.3f}")    print("命中统计:", {l.name: l.hit_count for l in loras})    print("常驻列表:", list(manager.loaded.keys()))  if __name__ == "__main__":    main()
目的:模拟一个多 LoRA 推理服务的核心链路——语义路由挑选面具、按需加载进“显存”、LRU 驱逐冷门适配器、低秩分支前向计算。
  1. self.B = np.random.randn(d, rank).astype(np.float32) * 0.1生成低秩矩阵 B,这是面具的“下半张脸”,随机初始化模拟已训练好的 LoRA。
  2. self.desc_vec = np.random.randn(k).astype(np.float32)每个 LoRA 的“简介向量”,路由时用来与请求做相似度比对。
  3. sim = float(np.dot(v, w))余弦相似度计算,决定这单活由谁接,对应检票员“察言观色”。
  4. if len(self.loaded) >= self.capacity:化妆台已满,进入 LRU 驱逐流程,请走最久没用的面具。
  5. victim = self.order.pop(0)弹出最久未使用的 LoRA 名字,并把它从常驻列表里删除,腾出显存。
  6. return lora.B @ (lora.A @ x)LoRA 前向增量计算,对应公式 $(BA)x$,先过 A 再过 B 减少中间矩阵。
  7. swap = manager.load(chosen)按需加载:命中则更新 LRU 顺序,未命中则触发换脸并返回 True。

常见误区

误区“每次换 LoRA 都要重新启动服务,不然权重不生效。我以前微调完模型都是重启再加载的。”

正解LoRA 的本质是低秩增量,热切换只需要在推理间隙更新权重矩阵(合并方式)或交换低秩分支(分离方式),不需要重启进程。就像给演员换面具,不需要让他重新从后台走到台上。

为何常见:全量微调时代确实要重启服务加载新模型,很多人把这一经验直接套到了 LoRA 上,忽略了 LoRA 的设计初衷就是“轻量热插拔”。

误区“反正显存够,把 LoRA 的秩设到 512,效果一定比秩 8 好得多。”

正解秩过大反而容易过拟合、训练不稳定,且推理时低秩分支计算量变大、切换更慢。实践中的常用区间是 r=8~64,关键看任务复杂度,不是越大越好。

为何常见:直觉上“参数多 = 能力强”,忽略了 LoRA 的哲学是“以小搏大”——用极小的参数量捕获任务的核心变化,贪多反而失真。

误区“换 LoRA 只是换权重,之前算好的 KV Cache 还能继续用,省掉重复计算。”

正解不同 LoRA 对同一段文本的注意力分布不同,KV Cache 混用会导致输出质量明显下降。工程上要么按 LoRA 维度分区缓存,要么切换后清空重建。

为何常见:只看到“权重换了但文本没变”,忽略了注意力分布已被 LoRA 改变的事实,把缓存当成了与模型无关的纯文本记忆。

误区“把 100 个 LoRA 全塞进显存,一起推理,也就是显存大点的事,没什么特别的。”

正解多租户还涉及路由隔离、配额控制和安全边界。尤其在共享 batch 里,每个请求必须携带 LoRA 标识,防止租户 A 的请求误用了租户 B 的 LoRA 权重。

为何常见:把“并发”简单理解为“都放进去一起跑”,没有考虑多租户场景下的资源治理与数据安全要求。

面试题

Q1. LoRA 中“低秩”的“秩”指的是什么?

单选题 · easy

  • A.神经网络层深度
  • B.增量矩阵的最大秩← 正确答案
  • C.每次训练的批量大小
  • D.输入序列的实际长度

答案:B。LoRA 假设权重增量 ΔW=B·A 能被 r 维低秩近似,r 就是矩阵秩的上限,远小于模型维度。选项 A 是网络结构概念,C 是训练超参数,D 是输入长度,都与“低秩”无关。

Q2. 一个基础模型挂载多个 LoRA 后,切换 LoRA 最合理的做法是?

单选题 · easy

  • A.重启模型进程
  • B.热切换更新权重← 正确答案
  • C.重新训练基础模型
  • D.复制整个模型副本

答案:B。热切换在推理间隙更新增量权重即可,进程不必重启。重启和复制整模型开销巨大,重训基础模型更是南辕北辙——换面具不该让演员重新练功。

Q3. 多 LoRA 服务中“路由调度器”的主要职责是?

单选题 · easy

  • A.训练LoRA适配器
  • B.按特征分派LoRA← 正确答案
  • C.压缩模型权重大小
  • D.标注高质量训练集

答案:B。路由负责根据请求特征(租户 ID、任务类型或语义向量)选择合适的 LoRA,像剧场检票员引路。训练、压缩、标注都属于其他环节,不是路由的职责。

Q4. 权重合并方式切换 LoRA 时,核心操作是?

单选题 · mid

  • A.W=W₀+αBA(合并增量)← 正确答案
  • B.W=W₀−αBA(撤销增量)
  • C.W=W₀×αBA(缩放叠加)
  • D.W=BA(替换原权)

答案:A。合并就是把低秩增量加到原始权重上:W_merged = W₀ + α(B·A)。减法是反方向修改,乘法与直接替换都不符合 LoRA 的定义,只有加法能正确“戴上面具”。

Q5. 关于 LoRA 热切换与 KV Cache,下列说法正确的是?

单选题 · mid

  • A.切换后KV Cache可随意复用
  • B.不同LoRA的Cache应分区缓存← 正确答案
  • C.KV Cache与LoRA完全无关
  • D.切换LoRA后必须重启服务

答案:B。不同 LoRA 改变注意力分布,旧缓存混用会降低输出质量,因此应分区缓存或重建。A 和 C 都忽略了 LoRA 对注意力的影响,D 则混淆了热切换与重启。

Q6. 显存预算公式 M_total = M_base + M_hot + M_swap 中,M_swap 指?

单选题 · mid

  • A.基础模型的权重占用量
  • B.常驻热LoRA的显存
  • C.冷门LoRA的搬运缓冲区← 正确答案
  • D.训练梯度所需的显存

答案:C。M_swap 是冷门 LoRA 临时搬运的交换区,用完即弃、循环复用。A 是 M_base,B 是 M_hot,D 是训练态概念,推理时不存在梯度占用。

Q7. 语义路由与标签路由的主要区别是?

单选题 · mid

  • A.按语义匹配而非标识查表← 正确答案
  • B.两者实现完全相同
  • C.语义路由生产不可用
  • D.标签路由需要神经网络

答案:A。语义路由把请求向量化后与 LoRA 简介向量算相似度;标签路由直接按租户 ID、任务类型查表。B 混淆两者,C 与 D 的说法均与事实相反。

Q8. 混批推理(如 S-LoRA)提升吞吐的核心原因是?

单选题 · hard

  • A.每个请求复制一份完整模型
  • B.共享基础计算,仅低秩分叉← 正确答案
  • C.通过减少LoRA总数提速
  • D.用CPU替代GPU执行推理

答案:B。混批让所有请求共享一次基础模型权重的前向计算,只在 LoRA 层按标识分叉到各自低秩分支,省去重复跑基础模型的开销。A 反而浪费资源,C、D 与吞吐提升无关。

Q9. 多租户推理环境中,为了保证租户隔离需要做到哪些?

多选题 · hard

  • A.维护租户与LoRA的映射关系← 正确答案
  • B.共享batch内携带LoRA标识← 正确答案
  • C.允许互读彼此LoRA权重
  • D.对每租户设置资源配额← 正确答案

答案:ABD。租户映射保证请求路由正确,batch 内标识防止串戏,配额控制防止一家挤爆显存。C 恰好是安全边界要禁止的行为,故选 A、B、D。

Q10. 以下哪些手段可以降低 LoRA 切换延迟?

多选题 · extreme

  • A.预热加载热门LoRA权重← 正确答案
  • B.流水线分批传输LoRA权重← 正确答案
  • C.对LoRA权重做量化压缩← 正确答案
  • D.切换时清空全部缓存

答案:ABC。预加载让热门面具提前到位,流水线传输边搬边算,量化压缩减小搬运体积,三者都直接缩短 T_load。D 清空全部缓存会增加 T_rebuild 并降低复用率,适得其反。