权重更新、LoRA 注册表与暂停控制面#

这章解决什么问题#

运行时架构如果只讲请求 steady-state、进程边界和模板解释层,仍然会少一层非常接近真实运维与平台控制的问题:模型权重怎么热更新?LoRA 适配器怎样注册、淘汰与复用?请求流量在更新期间怎样被暂停或让渡?

这一章就是把这条“热变更控制面”拉回运行时架构主线里。

为什么这层不该被丢进附录#

TokenizerManager 里和权重更新、LoRA、pause/resume 相关的代码并不是边缘工具,而是明确的运行时控制面:

  • init_weight_update()
  • model_update_lock
  • is_pause / is_pause_cond
  • update_weights_from_*
  • check_weights(...)
  • LoRARegistry

这说明系统并不假设模型权重和适配器在启动后永远不变。优秀技术书应该把这种“动态控制面”讲出来,因为它直接影响请求进入、运行和维护的边界。

一张图:热变更控制面位于请求主链的哪里#

这张图解决的理解障碍是:很多读者知道有权重更新和 LoRA,但不清楚它们如何和正常请求并存,而不是彼此互相踩踏。

flowchart LR
    Req["incoming request"] --> Pause["is_pause_cond / pause gate"]
    Pause --> Read["model_update_lock.reader_lock"]
    Read --> Run["tokenize -> scheduler/runtime"]
    Ctrl["update_weights / LoRA ops"] --> Write["model_update_lock.writer_lock"]
    Ctrl --> Registry["LoRARegistry / lora_update_lock"]
    Write --> Run
    Registry --> Run

图比纯文字多解释的一点是:请求流量和热变更不是两条互不相干的支线,而是围绕 pause gate、reader/writer lock 和 LoRA registry 共享同一运行时边界。

init_weight_update() 已经把控制面骨架写出来了#

TokenizerManager.init_weight_update() 会初始化:

  • model_update_lock = RWLock()
  • model_update_result
  • is_pause = False
  • is_pause_cond = asyncio.Condition()

这几个对象本身就说明设计意图:

  • 普通请求走 reader 侧。
  • 热更新走 writer 侧。
  • pause/resume 是请求进入前的外层闸门。

因此,权重更新不是“某个后台线程随时替换模型”,而是一个被显式同步原语包住的控制面。

普通请求怎样被这层控制面保护#

generate_request(...) 的关键顺序是:

  1. await self.is_pause_cond.wait_for(lambda: not self.is_pause)
  2. async with self.model_update_lock.reader_lock:
  3. 然后才解析 LoRA、tokenize 并发送请求。

这说明普通请求进入系统之前,至少要跨两道门:

  • pause gate:控制“现在能不能接新请求”
  • reader lock:控制“现在能不能在当前权重语义下继续读”

这是一种很典型的控制面分层:

  • pause 更粗。
  • reader/writer lock 更细。

update_weights_from_* 的真正含义#

tokenizer_communicator_mixin.py 里有三条权重更新路径:

  • update_weights_from_distributed(...)
  • update_weights_from_tensor(...)
  • update_weights_from_ipc(...)

它们的共通点不是输入来源,而是同步策略都围绕:

  • is_pause_cond
  • model_update_lock.writer_lock

这说明无论权重从哪里来,运行时都试图用统一的控制面语义包住“更新期间与请求流量的关系”。

为什么“已暂停”和“未暂停”要区分处理#

源码里会先检查当前是否已暂停:

  • 如果已经 pause,就尽量直接走更新 communicator。
  • 如果还没 pause,就在 writer lock 下执行更新。

这说明 pause/resume 不是单纯的用户界面开关,而是会影响热更新走哪条控制路径。

LoRARegistry 为什么是另一套控制面,而不是权重更新的附属物#

init_lora() 里会创建:

  • LoRARegistry(self.server_args.lora_paths)
  • lora_update_lock
  • lora_ref_cache

并且源码注释明确说,lora_update_lockmodel_update_lock 不同,它不会阻塞 inference,因此允许 LoRA 更新和推理重叠。

这点非常值得写进书里,因为它揭示了两个不同层级的控制面:

  • 基础权重更新更重,影响整个模型读语义。
  • LoRA 更新更轻,目标是尽量与推理重叠。

这不是实现巧合,而是对“更新代价不同、应有不同控制策略”的明确承认。

LoRA 路径里最重要的几个动作#

tokenizer_communicator_mixin.py 看,LoRA 路径至少包括:

  • register / unregister
  • wait_for_unload(...)
  • LRU 淘汰
  • acquire(...) / release(...)

这说明 LoRA 在运行时里不是一个简单字符串参数,而是一套有生命周期、有容量上限、有引用计数味道的资源对象。

对维护者来说,这比“支持 LoRA”四个字重要得多,因为它决定了:

  • 请求到来时能否安全拿到 adapter
  • 请求结束后 adapter 何时释放
  • registry 满时谁被淘汰

pause_generation(...) 为什么更像系统控制命令,而不是普通 API#

TokenizerManager.pause_generation(...) 会:

  1. 先把 is_pause = True
  2. 再尝试确认当前 writer/readers 是否已走到安全状态
  3. resume 时再 notify_all()

这说明 pause/resume 不是“把请求简单地 reject 掉”,而是为了给热变更、维护窗口或某些全局操作创造一个更稳定的切换点。

check_weights(...) 提供了什么能力#

TokenizerManager.check_weights(...) 会通过 check_weights_communicator 发到 scheduler 侧,再由 scheduler_update_weights_mixin.py::check_weights(...)model_runner.check_weights(...)。也就是说,系统不仅支持更新,还支持在控制面上主动验证当前权重状态。

这对大系统尤其重要,因为热更新真正难的往往不是“发命令”,而是“确认各 rank、各 worker 已经到达同一权重状态”。

这一层最容易出现的误判#

1. 把 LoRA 更新和基础权重更新当成同一种操作#

源码明确给它们配置了不同的锁语义和重叠策略。

2. 以为 pause 只是阻止新请求进入#

实际上它还在为 writer-side 控制操作创造稳定切换窗口。

3. 以为请求只要拿到 reader lock 就和 LoRA 无关#

并不对。请求仍然会在 _validate_and_resolve_lora(...) 里与 LoRA registry 交互。

如果热更新路径出问题,先怎么查#

建议按这个顺序:

  1. 看系统当前是否处在 is_pause 状态。
  2. model_update_lock 是 reader 侧堵住了,还是 writer 侧没拿到。
  3. 看具体走的是 distributed、tensor 还是 IPC 更新路径。
  4. 如果是 LoRA,先看 registry、acquire/release、LRU 与 lora_update_lock
  5. 最后再查 scheduler 侧 check_weights(...) 或 model runner 校验结果。

这套设计的收益与代价#

收益:

  • 把“接请求”和“改模型状态”明确分成两条受控路径。
  • 基础权重更新和 LoRA 更新能采用不同粒度的控制策略。
  • pause、reader/writer lock、registry 共同构成了较清晰的热变更控制面。

代价:

  • 控制面状态更多,排障时要同时理解 pause、锁与 registry。
  • 更新路径跨 tokenizer、communicator、scheduler、model runner 多层。
  • 如果维护者没有建立这层心智模型,容易把热更新问题误判成普通推理问题。

小结#

这一章真正要补齐的,是运行时架构里很容易被漏掉的“动态控制面”:

  • steady-state 之外,系统还要处理权重热更新与 LoRA 生命周期。
  • model_update_lockis_pause_condLoRARegistry 是这层的三根骨架。
  • 只有把这层看清楚,运行时架构才不只是“如何运行”,也包括“如何在运行中安全改变自己”。