对应 Maps to:Stanford CS349D AI Inference Infrastructure(Spring 2026),#18 Mon Jun 1 Multi-tenancy。必读:P. H. Coppock, B. Zhang, E. H. Solomon, V. Kypriotis, L. Yang, B. Sharma, D. Schatzberg, T. C. Mowry, D. Skarlatos, LithOS: An Operating System for Efficient Machine Learning on GPUs(SOSP 2025);S. Yu, Y. Qiao 等, Prism: Cost-Efficient Multi-LLM Serving via GPU Memory Ballooning(2025)。选读:X. Chai 等, Fork in the Road: Reflections and Optimizations for Cold Start Latency in Production Serverless Systems(OSDI 2025)。 依据说明:原课没有公开 slides。本讲依据三篇论文的全文(Prism 用的是 arXiv v3,2026 年 6 月版,含生产部署数据)。§1 的”三个层次”的框架、§6 的综合和 §7”之后的发展”是我补充的;Fork in the Road 讲的是通用的 serverless 函数平台,§5 里它与 LLM 冷启动的对应关系(引擎初始化、权重加载、LoRA 适配器与”树状种子”)是我的类比。讲义里的纸上数字全部来自配套代码的实际运行。 配套代码 Code:
code/lecture12/(numpy;4 个脚本 + 2 个模块:gpu_share.py是一块 GPU 内部、以 TPC 为单位的共享模拟器,multi_llm.py是多模型多 GPU 的显存共享模拟器;代码 03 约 35 秒,其余几秒) 预计阅读时间:3–4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说清 GPU 为什么普遍用不满(生产中的利用率数据),以及”多租户”要在哪三个层次上解决问题:一块 GPU 内部的算力、多个模型之间的显存、模型的启动。
- 比较 NVIDIA 提供的四种共享机制(时间片、MPS、流优先级、MIG):它们各在时间还是空间上共享、隔离有多强、会造成什么样的干扰。
- 解释 LithOS 的设计:按 TPC 分配算力、TPC 偷取、把内核透明地切成”原子”以减少排头阻塞、按内核”量体裁衣”分配 TPC、按内核的频率敏感度调频,以及它们为什么需要一个在线的延迟预测器。
- 解释 Prism 的出发点(生产负载的”突发组”特征让纯空间共享和纯时间共享都失败)和设计:显存”气球”(虚拟地址预留 + 物理页按需映射)统一空间与时间共享、引擎池与并行加载、按 KV 压力比放置、按 Moore–Hodgson 算法排队。
- 说出冷启动的延迟从哪里来(控制路径、资源争用、初始化),以及保温、池化、“从模板分叉”这些办法各在权衡什么;把它们对应到 LLM 的模型加载上。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 前置讲 P1:GPU 的 SM、显存层级、roofline | GPU 驱动与 CUDA 运行时的内部实现 |
| 前置讲 P3:操作系统的进程、虚拟内存、页表、调度 | Kubernetes 的设备插件、资源调度 |
| 第 2 讲:连续批处理;第 3 讲:PagedAttention 的块式 KV 管理 | LoRA 多适配器服务的内核(S-LoRA、Punica) |
| 第 11 讲:多副本集群的路由 | 训练集群的调度 |
0.3 本讲地图 Map
§1 为什么要多租户:GPU 用不满、模型越来越多、流量时有时无
§2 NVIDIA 的四种共享机制:时间片、MPS、流优先级、MIG ← 代码 01 演示 1–2
§3 LithOS:GPU 的操作系统——TPC 调度、内核原子化、量体裁衣、调频 ← 代码 01 演示 3–4、代码 02
§4 Prism:很多个 LLM 共享显存——"气球"、放置与排队 ← 代码 03
§5 冷启动:Fork in the Road 与 LLM 的模型加载 ← 代码 04
§6 三个层次的综合
§7 之后的发展
§8–12 小结、常见误区、练习、延伸阅读、下一讲预告
0.4 记号 Notation
| 符号 | 含义 |
|---|---|
| SM / TPC / GPC | 流式多处理器 / 线程处理簇(2 个 SM)/ 图形处理簇(若干 TPC);A100 有 108 个 SM = 54 个 TPC |
| HP / BE | 高优先级(延迟敏感,如在线推理)/ 尽力而为(如后台训练、离线批处理) |
| 原子 atom | LithOS 把一个内核的线程块切成的子集,可以单独调度 |
| LithOS 的内核扩展模型:在 个 TPC 上的延迟 | |
| slip | 允许的延迟倍数(1.1 = 最多慢 10%);调频模型里 = slip − 1 |
| 、 | 内核对频率的敏感度、按时间加权的总敏感度 |
| KVPR | Prism 的 KV 压力比 = SLO 加权的 KV 增长率之和 ÷ 剩给 KV 的显存 |
| TTFT / TPOT | 首 token 延迟 / 每个输出 token 的平均间隔(第 1 讲) |
0.5 关于数字 · About the numbers
本讲的数字有三类:(1) 内核级模拟(代码 01、02):一块 54 个 TPC 的 A100,内核延迟用 或”按 TPC 划分的 roofline”,HP 推理和 BE 训练的内核长度、TPC 上限都是假设值;(2) 集群级模拟(代码 03、04):多个 Llama 3.x 模型共享 H100,每一步的时间来自第 1 讲的纸上 GPU,负载是合成的”突发组”负载,激活时间(引擎初始化、加载带宽)是参照 Prism 报告的量级设的假设值;(3) 玩具功耗模型(代码 02)只用于比较趋势。论文里的数字写成”论文报告”。
1. 为什么要多租户 · Why multi-tenancy
1.1 GPU 用不满
GPU 很贵,可它在数据中心里的利用率低得惊人(LithOS §1、§3,论文报告):
- Meta 的一个广告推理服务(H100,每块用 MIG 切分)一周内:设备的计算利用率平均只有 27%(17%–40%),SM 利用率平均 14%,显存带宽利用率 20%;显存容量却一直占着 28%——为了满足严格的尾延迟 SLO,模型必须常驻、批必须小。
- 公开报告里,微软的平均 GPU 利用率 52%,阿里巴巴的中位数只有 10%。
- 流量有昼夜起伏(Meta 的数据里最高是最低的 2.2 倍);模型的热度差几百倍;模型大小差 10 倍以上。
Prism 从推理服务商的角度看到了同样的问题:Hugging Face、阿里云这类平台要托管成千上万个基础模型和微调模型,其中很多流量很小、却必须随时可用。行业惯例是给每个模型一组专用 GPU,保证响应、浪费容量;生产分析显示 GPU 的忙碌时间常常低于 30%。随着 token 价格下降,这种浪费越来越不可接受。
1.2 三个层次的问题
“多个任务 / 模型共用 GPU”听起来是一个问题,其实是三个不同层次的问题,本讲的三篇论文各管一层:
| 层次 | 共享的资源 | 难点 | 本讲 |
|---|---|---|---|
| 一块 GPU 内部 | SM / TPC(算力) | 在线服务的延迟不能被后台任务拖慢;GPU 没有好用的抢占 | LithOS |
| 多个模型之间 | 显存(权重 + KV cache) | 流量时有时无、此消彼长;静态划分浪费,换入换出又慢 | Prism |
| 模型的启动 | 时间(冷启动) | 不常用的模型不能一直占着显存,可每次加载要几秒到几十秒 | Fork in the Road(serverless) |
2. NVIDIA 的四种共享机制 · GPU sharing mechanisms
2.1 先认识 GPU 的层级
GPU 由若干 GPC 组成,每个 GPC 有若干 TPC,每个 TPC 有 2 个 SM,每个 SM 有几十到上百个核心(LithOS 图 2;例如 H100 的完整芯片是 8 个 GPC × 9 个 TPC × 2 个 SM)。程序以内核(kernel)为单位提交工作;一个内核被分成很多线程块(thread block),硬件把线程块派发到空闲的 SM 上执行。CUDA 的流(stream)里的内核按先进先出执行。
关键的限制:一个线程块一旦开始执行,就要跑完才会让出 SM(LithOS 说 GPU 缺少实用的硬件抢占)。一个内核可能只要几微秒,也可能要几十毫秒(LithOS 图 10:训练内核随批大小增长,DLRM 超过 30 毫秒;长提示的 LLM 推理也有几毫秒的内核)。
2.2 四种机制
| 机制 | 共享方式 | 隔离 | 问题 |
|---|---|---|---|
| 时间片(time slicing,默认) | 时间:轮流独占整块 GPU,每段几毫秒 | 时间上隔离 | 同一时刻只有一个任务在跑,利用率低;在线请求要等别人的时间片 |
| MPS(Multi-Process Service) | 空间:多个进程的内核同时执行,线程块可以挤在同一个 SM 上 | 几乎没有 | 吞吐最高,但互相干扰,延迟不可控 |
| 流优先级(stream priority) | 空间:高优先级流的线程块先派发 | 弱 | 已经在跑的低优先级线程块不能被抢占 |
| MIG(Multi-Instance GPU) | 空间:按 GPC 边界把 GPU 切成几个实例,算力、显存、带宽都分开 | 强(硬件隔离) | 粒度粗、配置固定(A100 只有几种切法),重新配置要 5 秒以上;一个实例空闲时别人用不了 |
研究系统大多在这几种机制之上做文章(LithOS §2.2):Clockwork、Clipper 等以整个推理请求为单位做时间共享;REEF、Orion 等在空间共享时限制 BE 内核的发射;TGS 对容器化的应用透明地做速率控制。它们的共同问题是粒度太粗(以请求、批或内核为单位),于是要么利用率低,要么排头阻塞(一个长的 BE 内核挡住 HP)。另一个实际障碍是透明性:很多方案要求改应用或框架(甚至绑定某个早已不维护的 PyTorch 版本),在大规模部署里不现实。
2.3 在模拟器里比较
代码 01 演示 1:一个 HP 推理服务(每秒 60 个请求,每个请求是 96 个内核、独占 GPU 时约 6 毫秒,GPU 约 40% 的时间在忙)+ 一个 BE 训练任务(内核平均 8 毫秒)共用一块 A100:
| 共享方式 | HP P99 / 独占 | BE 吞吐 |
|---|---|---|
| 时间片(2 ms) | 3.45× | 0.63 |
| MPS | 3.10× | 0.66 |
| 流优先级 | 8.71× | 0.65 |
| MIG(HP 31 / 54 TPC) | 2.36× | 0.43 |
| LithOS 不切原子 | 1.30× | 0.62 |
| LithOS | 1.00× | 0.61 |
(BE 吞吐 = BE 完成的工作 ÷ 它独占 GPU 时能完成的。)
- 时间片:HP 要等 BE 的时间片结束,自己也只能连续用一个时间片。
- MPS:HP 的每个内核都和 BE 分享 TPC,每个都变慢。论文报告 MPS 在推理 + 训练的组合下让 HP 尾延迟变成 5.83 倍、服务吞吐只剩 60%。
- MIG:HP 被关在 31 个 TPC 里,大内核都变慢;BE 只能用剩下的 23 个。HP 空闲时它的那一份也没人用——两头都浪费。
- 流优先级:模拟里最差。HP 的归一化内核只用 8 个 TPC,BE 的线程块趁机占住其余 46 个,一占就是一个线程块的时长;HP 的下一个大内核只好在 8 个 TPC 上慢慢跑。代码 01 演示 2 显示,它的表现完全取决于 BE 线程块有多长:50 微秒时 1.19 倍,0.5 毫秒时 8.71 倍,2 毫秒时 HP 处理不过来、延迟上百倍。(论文报告流优先级是 2.89 倍;我们取的线程块时长是假设值。)
这张表的意思是:光有”能同时跑”或”能分开”都不够。要同时保住 HP 的延迟和 BE 的吞吐,调度器需要更细的控制粒度——这是 LithOS 的出发点。
3. LithOS:GPU 的操作系统 · LithOS
论文卡片 Paper card · LithOS
- 问题:数据中心的 GPU 利用率很低,但把任务堆在一起又会互相干扰;现有方案要么粒度太粗(以请求或内核为单位),要么需要改应用。
- 关键想法:像 CPU 的操作系统管理核心一样管理 GPU:在 CUDA 驱动 API 这一层透明地拦截所有内核提交,由自己决定每个内核(甚至内核的一部分)何时、在哪些 TPC 上执行。四个机制:TPC 调度器(含 TPC 偷取)、内核原子化、硬件量体裁衣、透明调频。
- 实现:约 5,000 行 Rust,建立在 MPS 之上;支持 PyTorch、TensorFlow、JAX、TensorRT 以及 cuDNN 这类闭源库,不改模型、运行时或框架。
- 结果(论文报告):推理 + 推理时,HP 尾延迟比 MPS 低 13 倍、比最好的已有方案低 3 倍、总吞吐高 1.6 倍;推理 + 训练时,比 MPS 低 4.7 倍、比最好的已有方案低 1.18 倍、总吞吐高 1.35 倍;量体裁衣平均省下四分之一的 GPU 容量(性能代价 < 4%),调频平均省下四分之一的能量(性能代价 7%)。
- 局限:单块 GPU;不划分显存带宽和 L2 缓存;调频受限于现有 GPU 约 50 毫秒的频率切换延迟。
3.1 架构:把调度权从驱动和硬件拿回来
LithOS 提供一个动态链接库 LibLithOS,冒充原生的 CUDA 驱动库(LithOS 图 7、图 8)。应用照常调用 cuLaunchKernel,LithOS 把内核放进这个流对应的发射队列后立即返回——提交和执行被解耦了。为什么要”拦下来”?因为内核一旦交给 GPU,它的优先级、资源都不能再改,也不能重新调度;尽早下发会导致糟糕的调度。LithOS 只在需要时才把工作交给 GPU,并控制 GPU 上积压的工作量(太多会失去灵活性,太少会让 GPU 饿着)。
管理员可以给每个应用设置 TPC 配额:有工作时保证它至少能用这么多 TPC,就像操作系统保证一个进程能用几个 CPU 核心。
3.2 TPC 调度与偷取
按 TPC 调度。和 MIG 的静态划分不同,LithOS 可以让每个内核在不同的 TPC 集合上运行,切换没有重配置开销。配额保证了高优先级应用的资源,但固定的配额常常让 TPC 闲着(流量起伏、模型差异),于是有 TPC 偷取:把闲着的 TPC 借给别的任务(LithOS 图 9)。
偷取带来一个新问题:借出去的 TPC 被别人的长内核占着,主人回来时要等——这又是排头阻塞。LithOS 的对策是分层的:用一个在线延迟预测器给每个 TPC 设定时器,估计正在跑的工作什么时候结束,避免从”马上就要用”的 TPC 上偷;限制偷来的工作的数量;偷来的 TPC 上用更低的硬件流优先级。最根本的对策是下一节的原子化。
3.3 内核原子化:在没有抢占的 GPU 上实现”抢占”
既然线程块不能被抢占,那就让调度单位变小:把一个内核的线程块切成若干子集,叫原子(atom),每个原子单独提交、单独决定在哪些 TPC 上跑。原子结束时调度器就有机会重新分配 TPC。
怎样在不碰源码、不碰 PTX 的情况下做到?LithOS 不直接发射原内核,而是发射一个 prelude 内核(论文算法 1),用和原内核相同的网格配置和资源;每个线程块先算出自己的编号,只有编号落在这个原子的区间 [lo, hi) 里才调用原内核的入口,否则立即退出。例如一个 8 × 8 = 64 个线程块的卷积内核,切成两个原子就发射两次 prelude,区间分别是 [0, 32) 和 [32, 64);区间互不重叠,保证每个线程块恰好执行一次。切成几个原子 = 预测的内核时长 ÷ 可调的 atom_duration;切得太细,提前退出的线程块带来的开销会让内核变慢,所以对线程块很多、很短的内核,LithOS 会降低切分的力度或者不切。
代码 01 演示 3 复现了原子化的价值(LithOS 图 20 的纸上版):BE 训练的内核越来越长时(格式:HP P99 / 独占)——
| BE 内核平均长度 | 1 ms | 4 ms | 8 ms | 16 ms | 32 ms |
|---|---|---|---|---|---|
| LithOS 不切原子 | 1.02× | 1.08× | 1.30× | 1.52× | 2.50× |
| LithOS(原子 0.5 ms) | 1.00× | 1.02× | 1.00× | 1.02× | 1.00× |
不切原子时,HP 的请求到达时如果 BE 正在偷用 TPC,就要等这个 BE 内核跑完,内核越长等得越久;切成原子后最多等一个原子。代价是每个原子的固定开销:演示 4 里 0.1 毫秒的原子让 BE 吞吐从 0.62 掉到 0.57,4 毫秒的原子让 HP 的 P99 升到 1.10 倍。论文报告原子化在大批训练和长序列推理时分别把 HP 的 P95 延迟再降低 2 倍和 1.3 倍,HP 尾延迟在独占的 14% 以内;代价是约 10% 的 BE 吞吐。
(我们的模拟器对 LithOS 做了简化:HP 有请求时 BE 只能用配额以外的 TPC,HP 空闲时才偷用配额内的;论文的调度器还会用预测的内核时长在 HP 请求进行中偷取。)
3.4 量体裁衣与调频
有了按 TPC 的调度,LithOS 还能做两件”省资源”的事。
硬件量体裁衣(right-sizing)。不同内核的扩展性差别很大(LithOS 图 11):GEMM 和注意力随 TPC 增加收益递减,有的小内核(例如 LLM 的频率惩罚内核)根本不随 TPC 变快。所以”给整个模型选一个 TPC 数”不行,要按内核选。LithOS 用两次测量(全部 TPC、1 个 TPC)拟合
(形如 Amdahl 定律: 可以看成一个线程块在一个 SM 上跑完的时间, 衡量能利用多少并行),再找出延迟不超过 slip 倍(例如 1.1)的最少 TPC 数;同一个内核的原子继承这个分配。对那些线程块太少、难以建模的短内核,用”线程块数 ÷ 每个 TPC 能同时执行的线程块数”作为可用 TPC 数的上限。论文报告平均省下 26% 的 TPC·时间(最多 51%),P99 和吞吐的代价都是 4%,拟合的 R² 在 0.92–0.99 之间。
代码 02 用一个”按 TPC 划分的 roofline”作为真实延迟(算力随 TPC 线性增长,显存带宽在 24 个 TPC 处饱和,后者是假设值),检验两点拟合:
| 负载(Llama-3-8B 一层) | 两点拟合:省下 | 真实最优:省下 | 拟合方案的变慢 |
|---|---|---|---|
| prefill 2048 token | 0.3% | 0.6% | 1.079× |
| decode 批 8 | 20.6% | 56.4% | 1.001× |
| decode 批 64 | 17.8% | 51.3% | 1.005× |
| decode 批 256 | 0.9% | 1.4% | 1.086× |
compute-bound 的内核几乎省不了;memory-bound 的 decode 其实只需要一半左右的 TPC,但两点拟合只敢省下约三分之一:真实曲线在带宽饱和处有一个拐点, 是一条光滑曲线、表示不了它(R² 仍有 0.989,但在 24 个 TPC 处把延迟高估了一半)。方案是安全的,只是保守——这正是 LithOS 要加”线程块数 / 占用率”上限来修正的原因之一。
透明调频(DVFS)。right-sizing 是”横向”缩减,调频是”纵向”缩减:很多内核在降频时的变慢是可预测的,而 memory-bound 的内核几乎不受 SM 频率影响。LithOS 用一阶近似:内核在频率 下的相对变慢 , 是这个内核的敏感度;按运行时间加权得总敏感度 ,给定允许的变慢 ,选
compute-bound 的内核让频率靠近最大值,memory-bound 的内核让频率降得更低。现有 GPU 切换频率要约 50 毫秒,所以 LithOS 按”内核序列”的累计效果决定频率,并采取保守的学习策略:新内核先在最高频率下收集数据,然后假设线性、降一点,观察实际表现,再决定继续降还是停。论文报告平均省下 26% 的能量(最多 46%),P99 平均慢 7%。
代码 02 演示 3 说明了”保守学习”为什么必要(允许慢 10%,能耗用玩具功耗模型):
| 负载 | 敏感度 | 一阶外推 | 逐步试探 |
|---|---|---|---|
| prefill 2048 token | 0.984 | 1290 MHz / 慢 1.09× | 1290 MHz / 慢 1.09× |
| decode 批 8 | 0.000 | 210 MHz / 慢 1.01× | 210 MHz / 慢 1.01× |
| decode 批 64 | 0.032 | 345 MHz / 慢 1.89× | 630 MHz / 慢 1.08× |
批 8 的 decode 降到最低频率也几乎不变慢——decode 等的是显存,SM 跑得再快也没用(第 1 讲)。批 64 的 decode 在高频时测到的敏感度几乎为 0,一步外推到 345 MHz,可那时 SM 太慢、内核已经变成 compute-bound,慢了 1.89 倍;逐步试探在超标后二分,停在 630 MHz。
3.5 在线延迟预测
上面每个机制都依赖”这个内核要跑多久”:偷取要知道借出去的 TPC 何时空出,原子化要知道切几块,量体裁衣和调频要知道不同 TPC 数和频率下的延迟。LithOS 不做离线剖析(透明的操作系统做不到),而是在线学习:分别为每个发射队列记录内核的实际延迟。一个陷阱是同一个内核函数(例如卷积)在不同层里处理的张量大小不同、延迟也不同,所以要按”算子”而不是按”内核函数”记。LithOS 没有模型的数据流图,就利用应用的显式同步事件确定一个批的开始,用”批开始后的第 个内核”这个序号来唯一标识算子。论文报告 HP 负载的误预测率(误差超过 50 微秒)只有 0.9% 和 0.38%。
3.6 评测
论文报告(单块 A100-40GB):
- 推理 + 推理(一个延迟敏感的 HP、一个吞吐导向的 HP、一个 BE):MPS 总吞吐最高(1.08)但 SLO 达标只有 42%;MIG 和线程上限能保住 SLO,但没有 BE 时分区用不满,总吞吐只有 0.59 和 0.66;只有优先级的系统无法隔离两个 HP,最好的 TGS 达标 72%。LithOS 达标 100%、总吞吐 1.0,还给 BE 留出 0.15 的吞吐。
- 推理 + 训练:MPS 的 HP 延迟 5.83 倍、服务吞吐 60%;时间片和 MIG 大约各给服务一半 GPU、撑不住峰值;流优先级 2.89 倍;TGS 1.41 倍(它的速率控制假设到达率恒定);REEF 的尾延迟达到 8.93 倍。LithOS 把尾延迟保持在独占的 20% 以内,总吞吐提高 1.23–1.57 倍(平均 1.38 倍)。
- 消融:只开 TPC 调度器时 HP 尾延迟 1.38 倍(靠限制 BE),加上原子化后 1.19 倍。
代码 01 演示 4 还显示两点:给 HP 的配额少于整块 GPU 时(例如 31 / 54),HP 的延迟按比例变长(2.38 倍,和 MIG 相当)——配额是”保证 BE 至少有多少”和”HP 能快到什么程度”之间的旋钮;HP 负载变重时,MPS 先崩溃(每秒 90 个请求时 P99 27 倍),LithOS 始终保住 HP,代价是 BE 只能用剩下的时间(每秒 120 个请求时 BE 只剩 0.23)。
4. Prism:很多个 LLM 共享显存 · Prism
论文卡片 Paper card · Prism
- 问题:推理服务商要让很多 LLM(含大量低流量但必须可用的模型)随时可用。生产 trace 显示,活跃的模型集合在不停变化(“突发组”)、请求极不稳定;纯空间共享会让空闲模型锁住显存,纯时间共享会在交替的突发中反复换入换出(颠簸)。
- 关键想法:显存是统一的瓶颈。像虚拟化里的”内存气球”一样,按需从空闲模型回收显存、给活跃模型扩充 KV,或者换入新模型的权重——用一种机制同时支持空间共享和时间共享。
- 设计:气球驱动 kvcached(每个引擎预留一大段虚拟地址,物理页按需映射);引擎池 + 并行加载让模型激活在 1–2 秒内完成;全局按 KV 压力比放置模型;每块 GPU 内按松弛时间(Moore–Hodgson)排队。
- 结果(论文报告):基于 SGLang,在最多 32 块 H100、58 个模型、两份生产 trace 上,TTFT 达标率最高提高 3.3 倍、TPOT 达标率提高 2 倍;同样的达标率下成本降低 2 倍以上,或者多服务 3.5 倍的请求。kvcached 已开源,部署在超过 1 万块 GPU 的集群里;两家公司的影子流量对比显示每 GPU 吞吐提高 3.89 倍、每 GPU 收入提高 2.86 倍。
- 局限:一块 GPU 上的多个模型仍然共享算力,Prism 主要管显存;放置用的是长期的请求率估计,突发来得太快时仍要靠激活与迁移。
4.1 生产负载:突发组与剧烈波动
Prism 分析了四份 trace(Hyperbolic 24 个模型 4 个月、Novita AI 16 个模型 1 个月、Chatbot Arena 的对战与聊天,最多 129 个模型、最长 16 个月),发现两个层次的特征(论文 §3、附录 A.1):
- 模型层:变化的突发组。模型的请求以短促、不规则的突发出现,中间隔着长短不一的空闲;同一时刻只有一小部分模型活跃,而这部分在快速变化——就像程序的”工作集”。四份 trace 里平均 23%–50% 的模型同时活跃,活跃集合每小时变化 54–766 次;Novita 的模型平均 70% 以上的时间空闲。不同模型的模式也不同:大的通用模型常作为复合系统里的”中枢”持续活跃,小的蒸馏模型只在流水线的特定环节(工具调用、验证)被零星调用。
- 请求层:剧烈而不可预测。很多模型每分钟请求数的变异系数大于 1、每小时有 40–100 个超过 10 秒的空闲间隔;同一模型相邻两天同一时刻的流量几乎不相关(相关系数接近 0)——历史不能预测未来。
代码 03 演示 1 的合成负载(18 个模型、总请求率 20/秒)平均 42% 的模型同时活跃、活跃集合每小时变化 176 次,落在生产 trace 的范围里。
这两个特征分别击败了两种现有做法(论文 §3.3、图 2):
- 纯时间共享(例如 QLM、ServerlessLLM:模型轮流换入显存)在请求交替到来时反复换入换出,GPU 的时间花在 PCIe 传输和引擎重新初始化上,SLO 违约在负载高峰之前就开始激增。
- 纯空间共享(例如 MuxServe 或静态划分:多个模型常驻同一 GPU,各自占一份显存)在负载转为单个模型的突发时,空闲模型的权重和 KV 预留仍然锁着,突发的模型拿不到 KV 空间,排队、违约。Novita 的 trace 下,空间共享让一半的 GPU 显存”被占用却没用上”。
代码 03 演示 1 的静态划分也一样:3 块 GPU 上为 KV 划出的空间平均只用了 9%,可突发的模型仍然不够用。
4.2 显存气球:kvcached
关键观察:两种共享关心的是同一块显存的不同部分——时间共享关心怎样快速换入权重,空间共享关心怎样在同时服务的模型之间分配 KV cache。如果显存能像虚拟机的”内存气球”那样弹性伸缩(虚拟机监控器从客户机回收内存、再分给需要的客户机),就能在两种模式之间平滑切换,甚至同时使用(只对一部分常驻的模型做时间共享)。
难点在于现有引擎(SGLang、vLLM)的 PagedAttention(第 3 讲)是在应用层管理显存的:启动时就预分配一大块显存当 KV 块池,没有请求时也一直占着,而且一块张量的物理显存不能部分释放;不同模型的 KV 块形状(层数、头数、头维度)也不一样,没法放进同一个池子。Prism 提出四个要求:权重和 KV 之间能快速互换显存;不同结构的模型能共享 KV 显存;重新分配的开销和碎片都要小;对现有引擎和注意力内核透明。
kvcached 的解法是把显存管理下沉到 GPU 运行时这一层(论文 §5.2、图 4):
- 虚拟与物理分离:给每个引擎预留一大段连续的虚拟地址,让它以为自己有专用的显存;物理显存按 2 MB 的页按需分配、懒映射(基于 CUDA 的虚拟内存管理 API)。权重和 KV 统一管理,模型被换出时它的物理页立即可以给别人用;新模型激活时,同一块 GPU 上其他模型的物理显存上限被收紧。
- 自动映射 token 块:内部的 KV 管理器把不同模型的 token 块映射到虚拟页 / 物理页上,不同模型的块放在不同的物理页上,避免形状冲突。
- 降低开销与碎片:把一个 token 在所有层的 K、V 放在连续的虚拟地址里,一次批量分配就够(原来要 2L 次);后台线程预先准备一小批物理页;优先填满半满的页。
- 弹性张量(eTensor):通过 PyTorch 的扩展接口实现,用起来和普通张量一样,注意力内核不用改,兼容 CUDA graph。集成到 SGLang 只改了 22 行代码。
代码 03 演示 2 是论文图 6 的纸上版:一块 H100 上两个 8B 模型,前一分钟 A 忙、后一分钟 B 忙。静态划分给每个模型 19.9 GB 的 KV 配额,忙的那个顶到配额就排队、另一半空着,TTFT 达标率 0.926;弹性共享时忙的模型能用到 25 GB,达标率 1.000。
4.3 快速加载:引擎池与并行加载
时间共享的灵活性取决于换入有多快。重新激活一个模型要做两件事:初始化一个新的推理引擎(预留虚拟地址、建立分布式上下文等),以及从 CPU 内存加载权重;朴素地做要几十秒,远超在线推理几秒以内的 TTFT SLO(论文 §5.3)。
- 引擎池:现有系统里引擎和模型同生共死,每次激活都要付全部初始化代价。Prism 把两者解耦:每块 GPU 维护一池预先初始化好的引擎(虚拟地址空间、分布式上下文都已建好);模型被换出时只释放物理显存,引擎回到池里。由于不同模型的显存布局不同,一个 KV 虚拟内存管理器负责把预留的虚拟空间对齐到新模型需要的布局。
- 并行加载权重:直接用
cudaMemcpyAsync往一块 GPU 拷贝,即使多线程也用不满 CPU–GPU 互连(发往同一块 GPU 的拷贝会被串行化)。Prism 把权重切成小块,同时从同一台机器的多块 GPU 读入,再经 NVLink 汇总到目标 GPU;按张量流式加载,每块 GPU 只需约 30 MB 的缓冲。
论文报告(H100):1B–8B 模型在 0.7 秒内激活,14B 1.3 秒,70B 以上 1.5 秒。代码 03 演示 4 显示了快速加载的决定性作用:
| 配置(18 个模型、2 块 H100、20 请求/秒) | TTFT 达标 | TPOT 达标 |
|---|---|---|
| 弹性 KV,全部常驻(MuxServe++) | 0.817 | 1.000 |
| + 空闲换出 / 按需换入,普通加载(约 15 秒),先来先服务 | 0.701 | 0.944 |
| + 引擎池与并行加载(8B 约 0.8 秒) | 0.994 | 0.944 |
| + Moore–Hodgson 排队(完整 Prism) | 0.998 | 0.945 |
加载慢的时候,换入换出比全部常驻还差——每次激活的十几秒里来的请求全部超时。
4.4 以显存为中心的控制平面
把模型放置、请求调度、显存分配放在一起做联合优化(例如整数规划)是不可行的: 个模型、 块 GPU 就有 种放置,请求的执行顺序更是阶乘级的,而且一切都要在线决定、看不到未来的突发。Prism 的洞察是:模型、请求、token 都通过”消耗和争夺显存”影响系统,SLO 达标与否直接取决于执行时有没有显存。于是用两层调度”控制显存争用”(论文 §6):
全局:按 KV 压力比放置(论文算法 1)。目标是让每块 GPU 都留有让气球膨胀的余地——如果同一个突发组里的几个模型被放在同一块 GPU 上,它们会同时要 KV 空间。定义
分子是”SLO 加权的 KV 增长速度”(输入 token 和 decode token 都算,SLO 用 TPOT 的,因为 decode 对显存争用最敏感),分母是这块 GPU 放完权重后剩下的显存。算法把模型按需求从大到小排序,依次放到”放进去以后 KVPR 最小”的 GPU 上——高需求的模型自然和低需求的模型搭配(“需求互补”)。如果新位置和当前位置的 KVPR 差别不超过阈值 τ,就不迁移,避免不必要的开销。论文用类似 Graham 调度界的论证说明贪心的最大 KVPR 不会比最优差太多。
换出与换入:模型空闲超过一个阈值、而显存又紧张时被换出(论文的敏感性分析给出约 45 秒最好:太短会在请求间隙里反复换出换入,太长会占着显存);有新请求时立刻在 KVPR 最低的 GPU 上激活。迁移时源实例继续服务,等目标就绪再切换,权重和 KV 可以走 NVLink(8B 模型约 20 毫秒)。
本地:按松弛时间排队(论文算法 2)。同一块 GPU 上的模型争夺 KV 显存时,如果每个引擎各自贪心地接纳请求,一个高请求率、宽松 SLO 的模型会占满显存,饿死 SLO 严格的模型;给每个模型设显存上限又很难定(保守了浪费,宽松了没用),而且调整上限要等正在跑的请求结束才生效。Prism 改用每块 GPU 一个共享的请求队列:已知每个请求的 prefill 截止时间(到达时刻 + TTFT SLO)和执行时间(提示长度 ÷ 分块 prefill 的速度),就变成了经典的单机调度问题,用 Moore–Hodgson 算法最小化违约数:按截止时间排序依次加入,一旦当前请求会违约,就把已选请求里执行时间最长的那个踢出去(腾出的时间最多)。例如四个请求(截止时间 / 执行时间,秒)A 1.0 / 0.4、B 1.2 / 0.5、C 1.5 / 0.8、D 1.9 / 0.3:按截止时间顺序执行时 C、D 都违约;Moore–Hodgson 踢掉 C,A、B、D 都按时完成(练习 5)。
4.5 评测与我们的复现
论文报告(4 台机器 × 8 块 H100):8 个模型共用 2 块 GPU 时,Prism 在 99% 的 TTFT 达标率下能多承受 2.3 倍(对 MuxServe++)和 3.5 倍(对静态划分)的请求;随着 SLO 放宽,Prism 很快达到 99%,其他系统在最宽的 SLO 下也达不到(最好的 MuxServe++ 84.79% / 67.22%);18 个模型时 Prism 用 4–5 块 GPU 达到 99%,基线用 8 块也达不到;58 个模型(含张量并行的 32B、70B)时 Prism 用 16 块 GPU 接近 99%,MuxServe++ 要 32 块。最坏情况(恒定负载、没有共享的机会)下弹性显存的开销是 TTFT / TPOT 增加 3%–5%。
代码 03 演示 3(18 个模型,权重共约 123 GB;TTFT SLO 1 秒、TPOT SLO 30 毫秒;格式:TTFT 达标 / TPOT 达标):
| 请求率 20/秒 | 静态空间共享 | 弹性 KV | 时间共享 | Prism |
|---|---|---|---|---|
| 2 块 H100 | 0.307 / 1.000 | 0.817 / 1.000 | 0.303 / 0.968 | 0.998 / 0.945 |
| 3 块 | 0.903 / 1.000 | 1.000 / 1.000 | 0.474 / 0.996 | 1.000 / 0.999 |
| 4 块 | 0.996 / 1.000 | 1.000 / 1.000 | 0.784 / 1.000 | 1.000 / 1.000 |
达到 99% 的 TTFT 达标率,Prism 需要 2 块 GPU,弹性 KV 3 块,静态划分 4 块,时间共享 4 块也不够。2 块 GPU 放下全部权重后只剩约 21 GB 给 KV;Prism 只让当前活跃的模型常驻,KV 空间就宽裕得多。
和论文不一致的一点:Prism 在 2 块 GPU 时的 TPOT 达标率(0.945)低于静态划分和弹性 KV(1.000)。原因是它让更多请求同时运行——同一块 GPU 上轮转的活跃模型更多、批更大——而其他策略的请求大多在排队,一旦开始反而跑得快。我们的模拟器让一块 GPU 一次只执行一个模型的一步(模型之间没有并发重叠),可能放大了这个代价。另外,Moore–Hodgson 排队在这里只多了 0.004,因为所有模型的 SLO 相同;论文在 SLO 不同的实验里报告它让严格 SLO 模型的达标率提高 40% 以上。
5. 冷启动:Fork in the Road 与 LLM 的模型加载 · Cold starts
论文卡片 Paper card · Fork in the Road(AFaaS)
- 问题:serverless 平台按需启动函数实例,冷启动要几百毫秒到几秒,而函数本身常常只跑 50–100 毫秒。学术界的优化(检查点恢复、从模板分叉、缓存热实例、精简运行时)在蚂蚁集团的生产环境里效果打了折扣。
- 发现:三个被忽视的延迟来源——控制路径(高层与低层容器运行时之间的交互)、资源争用(高并发和持续运行时内核锁的争用)、用户代码初始化。
- 办法:用插件式的函数运行时接口(FRI)缩短控制路径;把争用的资源池化或从种子继承;把用户代码也做成种子,用树状的种子和多级分叉平衡延迟与内存。
- 结果(论文报告):端到端延迟比 Catalyzer 低 1.80–8.14 倍,启动时间 5.45–9.41 毫秒;24 倍并发下启动时间 6.97–14.55 毫秒(Catalyzer 38.39–74.05 毫秒);生产运行 18 个月以上。
5.1 生产里的冷启动
蚂蚁集团的公有云上有 5 万多个函数、每天约 1 亿次调用(论文 §2.1)。虽然 95% 以上的请求是热启动,但热请求集中在少数高频函数上:一半以上的函数冷启动概率超过 0.75,35% 的函数每次都是冷启动。为了节省内存,热实例只保留 1 分钟(AWS、Azure、OpenWhisk 分别保留约 10、20、30 分钟),所以优化冷启动本身仍然必要;80% 的请求的用户代码在 221 毫秒内执行完,冷启动的几百毫秒很显眼。
5.2 三个被忽视的延迟来源
一个函数实例的启动分三段(论文图 1、图 6):控制路径交互(containerd 这样的高层运行时请求低层运行时创建实例)、容器初始化(以往研究的重点)、用户代码初始化。论文用一个 Node.js 函数做了分解:
- 控制路径:按 OCI 规范,containerd 先通过 RPC 找一个 shim 进程,shim 再按”二进制调用”约定加载低层运行时的可执行文件、再通过 RPC 让它去分叉沙箱。即使沙箱本身亚毫秒就能分叉出来,每次调用都要加载一次运行时二进制(18.02 毫秒),控制路径占冷启动的 30%–40%(18–25 毫秒)。
- 资源争用:串行时一切都快,并发时就不一样了:clone() 要创建新的网络和 IPC 命名空间,争用内核锁,从 1.45 毫秒恶化到最坏约 418 毫秒;IPC 命名空间要挂载消息队列,超级块缓存命中率低,在 sb_lock、mount_lock 上排队;网络准备、协议栈初始化、seccomp 规则的编译和安装也都在高并发下抖动。持续高并发一小时后,吞吐从每秒 110 个函数降到 45 个。
- 用户代码初始化:加载语言运行时、编译代码、加载依赖库,常常比函数本身还慢;那个 Node.js 函数的 275.53 毫秒里有 238.73 毫秒花在加载和初始化依赖上。
5.3 AFaaS 的三个办法
- 缩短控制路径(FRI):把跨运行时的交互分成两类:只是转发请求的”shim 调用”(例如 fork())和真正干活的”服务调用”(例如 create())。前者用插件内的函数调用代替二进制调用:只有最初创建根种子时需要加载运行时二进制,之后的 fork()、activate() 都由高层运行时直接调用。代价是牺牲一点 OCI 的通用性。
- 池化与共享争用的资源:能预先分配的(veth 网卡对、cgroup)放进池子,用时取、用完回收(论文预分配了 1,000 个 veth、600 个 cgroup);能从种子继承的(网络、IPC 命名空间,在安全容器里由客户机操作系统隔离,所以可以共享)直接共享;seccomp 规则在准备种子时就编译好;网络协议栈拆成可共享(时钟、随机种子、协议处理器、控制块、ARP 表)和不可共享(IP、MAC 地址)两部分,前者在种子里初始化。
- 树状种子与多级分叉:把用户代码也预先初始化进种子(第 2 级),语言运行时是第 1 级,客户机操作系统是第 0 级(根)。子种子用写时复制共享父种子的内存,比每个函数一份完整种子省 28.11%–84.91% 的内存;有用户代码种子就从它分叉,没有就从语言运行时种子分叉,“尽力而为”。生产中各级种子的内存开销是 6.9–135.03 MB。
此外还有一些生产细节:函数处理器返回就提前销毁容器(关机可能要 10 秒以上);分叉时复制扩展页表并设为只读,避免大量 VM-exit。部署经验里有一条很值得记住:分叉会复制”本应唯一”的状态——gVisor 为了减少系统调用而缓冲的随机数在分叉出的实例里会重复,必须在分叉后重新初始化(Firecracker 对快照恢复也有同样的警告)。
代码 04 演示 3 用一个排队模型说明争用为什么随并发恶化:每个冷启动做 5 毫秒可并行的工作、1 毫秒必须持有一把全局锁的工作,24 个同时进行时 P99 从 6.20 毫秒升到 24.81 毫秒(4 倍);把锁内的工作减到 0.05 毫秒(池化的效果),P99 只有 5.12 毫秒。
5.4 对应到 LLM:模型的冷启动
Fork in the Road 讲的是通用的函数平台,但它的三个来源在 LLM 推理里都有对应(下面的对应关系是我的类比):
| serverless 函数 | LLM 推理 |
|---|---|
| 控制路径、容器初始化 | 推理引擎初始化:CUDA 上下文、显存池、编译、CUDA graph 捕获(Prism 的引擎池解决的就是它) |
| 用户代码初始化(加载依赖) | 加载权重:几 GB 到几百 GB,取决于从哪一层存储读 |
| 资源争用 | 多个模型同时冷启动时争 PCIe / 网络带宽、争引擎池 |
| 树状种子、写时复制 | 共享基础模型 + 小的 LoRA 适配器 |
| 热实例保留时间 | 模型的保温 / 换出阈值(Prism 约 45 秒) |
代码 04 的几个数字(假设值见代码):8B 模型每次新建引擎约 16 秒,引擎池 + 并行加载约 0.8 秒,从 10 Gb/s 网络拉权重要 13 秒以上;在突发组负载上,只要保温 10 秒就能消掉 99.8% 的冷启动(突发内部的请求间隔很短),剩下的发生在突发之间,要保温几百秒才能消掉,而那时保温占用的显存·时间已经接近 18 个模型全部常驻;20 个 8B 的微调版本各存一份要 321 GB,共享基础模型 + 秩 16 的 LoRA 适配器只要 16.6 GB,换一个版本只需加载 27 MB。
6. 三个层次的综合 · Putting the three layers together
| LithOS | Prism | AFaaS | |
|---|---|---|---|
| 共享 / 管理的资源 | 一块 GPU 的 TPC 与频率 | 集群里每块 GPU 的显存 | 节点上的启动路径 |
| 时间尺度 | 微秒到毫秒(原子、内核) | 秒到分钟(请求、模型的活跃期) | 毫秒(一次冷启动) |
| 共同的手法 | 把”提交”和”执行”解耦,自己掌握调度 | 把”虚拟”和”物理”解耦,按需分配 | 把”准备”和”使用”解耦,提前池化 |
| 让它可行的前提 | 在线的内核延迟预测 | 快速的激活(引擎池 + 并行加载) | 可共享 / 可继承的资源与种子 |
| 透明性 | 拦截 CUDA 驱动 API,不改应用 | 弹性张量,注意力内核不改 | 替换 OCI 的一部分接口 |
三篇论文有一个共同的模式:静态划分浪费,完全不隔离又互相干扰;解法是在更细的粒度上动态分配,而让动态分配可行的关键,是把某种”准备工作”挪出关键路径——LithOS 预测内核时长、Prism 预建引擎、AFaaS 预建资源池和种子。它们也互相补充:Prism 管显存不管算力(同一块 GPU 上的多个模型仍然争 SM),LithOS 管算力不管显存(应用各自占显存),一个完整的多租户推理平台两者都需要,再加上足够快的冷启动。
7. 之后的发展 · Since then
- 显存弹性进入生产。Prism 的 kvcached 已开源并在超过 1 万块 GPU 的集群里部署(论文 v3);更早的 vAttention、vTensor 也用 CUDA 虚拟内存 API 把虚拟和物理显存分开,不过目的是简化单模型服务的编程。
- 多模型的 GPU 池化。Aegaeon(SOSP 2025)面向模型市场做 GPU 池化,借助 prefill / decode 解耦在模型之间做细粒度的时间共享(Prism 论文 v3 把它列为时间共享的代表,并指出它需要复制权重);ServerlessLLM(OSDI 2024)用多级检查点存储和本地性感知的调度降低 LLM 的冷启动。
- GPU 的抢占式调度。GPreempt(ATC 2025)、XSched(OSDI 2025)等研究在不同加速器上提供更通用的抢占;LithOS 的原子化是不改硬件、不改应用的一种做法。
- 多 LoRA 服务。S-LoRA、Punica 等系统让一块 GPU 同时服务成百上千个 LoRA 适配器(代码 04 演示 4 的出发点),这在”很多个微调模型”的场景里往往比换入换出整模型更划算。
8. 小结 · Summary
- GPU 普遍用不满(Meta 的广告推理平均 27%),但多租户要在三个层次上解决不同的问题:一块 GPU 内部的算力(LithOS)、多个模型之间的显存(Prism)、模型的启动(冷启动)。
- NVIDIA 的四种机制各有短板:时间片利用率低、MPS 没有隔离、流优先级收不回已经在跑的线程块、MIG 粒度粗且空闲时浪费。代码 01 里它们让 HP 的 P99 变成 2.36–8.71 倍。
- LithOS:拦截 CUDA 驱动 API,按 TPC 调度、偷取闲置 TPC;用 prelude 内核把内核透明地切成原子,在没有抢占的 GPU 上限制排头阻塞;按 给每个内核量体裁衣;按频率敏感度调频;一切依赖在线的延迟预测。代码 01 里 HP 回到独占水平、BE 仍有 0.61 的吞吐;代码 02 显示两点拟合在 memory-bound 内核上偏保守、一阶调频外推可能降过头。
- Prism:生产负载是变化的突发组,纯空间共享锁死显存、纯时间共享颠簸;kvcached 的显存气球(虚拟预留 + 物理页按需)统一两者;引擎池 + 并行加载让激活在 1–2 秒内完成;KVPR 放置让每块 GPU 留出膨胀余地;Moore–Hodgson 排队减少违约。代码 03 里达到 99% 的 TTFT 达标率,Prism 需要 2 块 GPU,静态划分 4 块;慢加载时换入换出反而有害。
- 冷启动:控制路径、资源争用、初始化三个来源;池化、共享、树状种子把准备工作挪出关键路径。对应到 LLM:引擎初始化、权重加载、保温策略和”基础模型 + LoRA”。
9. 常见误区 · Common pitfalls
- “MPS 吞吐最高,所以是最好的共享方式。” 它没有隔离,在线服务的延迟会被拖慢几倍,负载一高就崩溃(代码 01 演示 1、4)。
- “MIG 隔离最好,延迟敏感的服务用 MIG 就行。” 它把服务关在固定的一份里,服务自己的大内核也变慢,空闲时那一份别人用不了(代码 01 演示 1)。
- “给在线服务设高优先级就能保住延迟。” 优先级只决定派发顺序,收不回已经在跑的线程块;线程块一长就失效(代码 01 演示 2)。
- “原子切得越细越好。” 每个原子有固定开销,太细会拖慢 BE;原子只需要比 HP 能容忍的等待短(代码 01 演示 4)。
- “memory-bound 的内核给少点 TPC 没关系,两点拟合就能找到最少需要多少。” 带宽饱和处的拐点让 偏保守(代码 02 演示 2)。
- “在一个频率测出敏感度很低,就可以一路降到最低频。” 降频后内核可能变成 compute-bound;要逐步试探(代码 02 演示 3)。
- “把空闲模型换出去总是有益的。” 只有激活足够快时才有益;激活要十几秒时,换入换出比全部常驻还差(代码 03 演示 4)。
- “保温时间越长越好。” 突发内部的冷启动几秒的保温就能消掉,突发之间的要几百秒,那时显存代价已经接近全部常驻(代码 04 演示 2)。
10. 练习 · Exercises
练习 2、3、5、7 的参考答案里的数字是用
gpu_share.py、multi_llm.py的函数实际算出来的。
练习 1(GPU 的层级与 MIG)。A100 有 108 个 SM。(a) 它有多少个 TPC?(b) MIG 按 GPC 边界切分,A100 最多切成 7 份;为什么 LithOS 的实验里 HP A / HP B 的 75% / 25% 的划分在 MIG 上做不到,只能用 4/7 / 3/7?(c) 为什么 LithOS 选择 TPC 而不是 SM 作为分配单位?
参考答案
(a) 54 个(每个 TPC 2 个 SM)。(b) MIG 的实例大小是固定的几种(1/7、2/7、3/7、4/7、7/7 等),只能按 1/7 的整数倍切;75% 不是 1/7 的整数倍,最接近的组合是 4/7 + 3/7。而且重新配置要 5 秒以上,不能随负载调整。(c) TPC 是硬件上能控制线程块派发位置的较细粒度(LithOS 通过限制可用的 TPC 来实现空间划分);它比 GPC(MIG 的粒度)细得多,又和硬件的组织方式对齐。
练习 2(right-sizing 的公式)。一个内核在 1 个 TPC 上跑 10 毫秒,在 54 个 TPC 上跑 0.4 毫秒。(a) 拟合 。(b) 允许慢 10%,最少需要几个 TPC?(c) 省下多少 TPC·时间?
参考答案
(a) , 毫秒。(b) 目标延迟 0.44 毫秒:,取 45 个(rightsize_tpcs 也给出 45,此时延迟 0.436 毫秒)。(c) 原来 54 × 0.4 = 21.6 TPC·毫秒,现在 45 × 0.436 ≈ 19.6,省下约 9%。这个内核的并行度很好( 很小),所以省得不多;right-sizing 的大收益来自 相对大或者用不满 GPU 的内核。
练习 3(调频)。一串内核按时间加权的总敏感度 ,允许慢 10%。(a) LithOS 的模型选多高的频率(A100 最高 1410 MHz,可选频率是 210 MHz 起每 15 MHz 一档)?(b) 为什么 compute-bound 的内核会把频率”拉向”最大值?
参考答案
(a) MHz,向上取到 1065 MHz(dvfs_frequency 给出 1065)。(b) compute-bound 的内核 ,它占的时间权重越大, 越接近 1, 越接近 ;memory-bound 的内核 ,只增加分母里的”可以降”的空间。所以一个负载里 compute-bound 的部分决定了能降多少。
练习 4(LithOS 的原子化)。一个 BE 内核有 64 个线程块、预计跑 16 毫秒,LithOS 的 atom_duration 设为 0.5 毫秒。(a) 它大约切成几个原子?每个原子覆盖哪些线程块?(b) 如果这个内核只有 8 个线程块呢?(c) 为什么切得太细反而会让内核变慢?
参考答案
(a) 16 / 0.5 = 32 个原子,每个覆盖 2 个线程块(例如 [0, 2)、[2, 4)……),每个原子是一次 prelude 内核的发射,编号不在区间内的线程块立即退出。(b) 最多只能切成 8 个原子(每个原子至少一个线程块),每个约 2 毫秒——原子的下限是一个线程块的时长,这也是”线程块很长的内核”难以调度的根本原因。(c) 每个原子都要发射一次 prelude 内核,网格里所有线程块都要启动、检查编号、提前退出;原子越多,这些”空转”的线程块越多,还有发射开销。所以 LithOS 会对线程块很多的内核降低切分力度。
练习 5(Moore–Hodgson)。同一块 GPU 上排着四个请求(截止时间 / 执行时间,秒):A 1.0 / 0.4、B 1.2 / 0.5、C 1.5 / 0.8、D 1.9 / 0.3,当前时刻是 0。(a) 按截止时间先后(EDF)依次执行,哪几个违约?(b) 按 Moore–Hodgson 算法呢?(c) 为什么要踢掉”执行时间最长”的那个,而不是”刚加进来”的那个?
参考答案
(a) A 在 0.4 完成、B 在 0.9、C 在 1.7(超过 1.5,违约)、D 在 2.0(超过 1.9,违约):违约 2 个。(b) 依次加入 A(0.4)、B(0.9)、C(1.7 > 1.5,踢掉已选中执行时间最长的 C,回到 0.9)、D(1.2 ≤ 1.9):A、B、D 按时完成,只有 C 违约(moore_hodgson 给出同样的结果)。(c) 踢掉最长的,腾出的时间最多,让后面的请求最有机会按时完成;可以证明这样得到的按时完成数最多。在 LLM 服务里,这意味着一个很长的提示在拥挤时会被往后放,换来更多短请求按时拿到首 token——这是一个”最大化达标数”而不是”公平”的目标。
练习 6(KVPR 放置)。两块 GPU,每块放完权重前有 72 GB。三个模型:X(8B,16 GB 权重,需求 3 个单位)、Y(8B,16 GB,需求 1 个单位)、Z(1B,3 GB,需求 2 个单位)。按 Prism 的算法 1 放置。
参考答案
按需求从大到小:X(3)、Z(2)、Y(1)。X:两块 GPU 都空,放进去后 KVPR 都是 3 / (72 − 16) = 0.054,平局取 GPU 0。Z:放 GPU 0 是 (3 + 2) / (56 − 3) = 0.094,放 GPU 1 是 2 / 69 = 0.029,选 GPU 1。Y:放 GPU 0 是 4 / 40 = 0.1,放 GPU 1 是 3 / 53 = 0.057,选 GPU 1。结果 GPU 0 只有 X,GPU 1 有 Z、Y:需求最大的模型独占一块 GPU、留出最多的 KV 空间,需求小的模型挤在一起——这就是”需求互补”。
练习 7(LoRA 适配器有多大)。Llama-3-8B(32 层, = 4096,K、V 的维度是 8 × 128 = 1024)。在 q、k、v、o 四个投影上加秩为 16 的 LoRA,每个矩阵增加 个参数,BF16。(a) 一个适配器多大?(b) 20 个微调版本,共享基础模型时一共要多少显存?
参考答案
(a) 每层:q、o 各 16 × (4096 + 4096) = 131,072,k、v 各 16 × (4096 + 1024) = 81,920,合计 425,984 个参数;32 层 1363 万个参数,BF16 约 27 MB。(b) 16.06 GB 的基础模型 + 20 × 27 MB ≈ 16.6 GB(代码 04 演示 4),而 20 份完整权重要 321 GB。
练习 8(思考:LithOS 与 MIG 的隔离差在哪里)。LithOS 在延迟和吞吐上都优于 MIG,那 MIG 还有什么理由存在?
参考答案(要点)
MIG 是硬件隔离:每个实例有自己的显存容量、显存带宽、L2 缓存切片和故障域,一个租户的显存访问不会挤占另一个的带宽,一个实例出错不会波及别的实例;这对互不信任的云租户、或对带宽极敏感的负载很重要。LithOS 只分配 TPC(以及频率),各应用仍共享显存带宽和 L2,隔离依赖进程的地址空间和对常见错误的拦截。所以二者可以组合:用 MIG 切出几个硬件隔离的大块,在每个块里用 LithOS 这类软件调度做细粒度的共享。
练习 9(思考:把 Prism 和 LithOS 放在一起)。Prism 让多个模型共享一块 GPU 的显存,但它们的计算仍然是轮流或并发地抢 SM。设想把两者结合,会带来什么好处?会有什么新问题?
参考答案(要点)
好处:Prism 决定”谁常驻、KV 怎么分”,LithOS 决定”常驻的模型怎样分 TPC”——例如给 SLO 严格的模型保证 TPC 配额、让宽松的模型偷用空闲 TPC,decode 这种 memory-bound 的步骤只给一半 TPC(right-sizing)、甚至降频省电;这能缓解代码 03 里 Prism 的 TPOT 代价(同一块 GPU 上活跃模型越多,每个模型的步间隔越长)。新问题:两层调度要协同——Prism 的放置用”KV 压力”衡量一块 GPU 的拥挤程度,还需要把”算力压力”也考虑进去;LithOS 的延迟预测要应对模型频繁换入换出(新模型的内核没有历史数据);两者的控制环时间尺度不同(秒级 vs 毫秒级),需要明确谁服从谁。
11. 延伸阅读 · Further reading
本讲的必读材料
- P. H. Coppock, B. Zhang, E. H. Solomon, V. Kypriotis, L. Yang, B. Sharma, D. Schatzberg, T. C. Mowry, D. Skarlatos. LithOS: An Operating System for Efficient Machine Learning on GPUs. SOSP 2025(arXiv:2504.15465)。
- S. Yu, Y. Qiao, M. Ma, Y. Li, S. Yang, X. Tong, Y. Wang, Z. Xie, Y. An, S. Cao, K. Bao, D. Vij, X. Ding, Y. Wang, Q. Lu, Z. Wang, G. Gao, H. Xu, J. Shu, J. Xing, Y. Sheng. Prism: Cost-Efficient Multi-LLM Serving via GPU Memory Ballooning. arXiv:2505.04021(v3,2026)。
选读
- X. Chai 等. Fork in the Road: Reflections and Optimizations for Cold Start Latency in Production Serverless Systems. OSDI 2025。
相关工作
- M. Han, H. Zhang, R. Chen, H. Chen. Microsecond-scale Preemption for Concurrent GPU-accelerated DNN Inferences(REEF). OSDI 2022。
- F. Strati, X. Ma, A. Klimovic. Orion: Interference-aware, Fine-grained GPU Sharing for ML Applications. EuroSys 2024。
- J. Duan et al. MuxServe: Flexible Spatial-Temporal Multiplexing for Multiple LLM Serving. ICML 2024。
- Y. Fu et al. ServerlessLLM: Low-Latency Serverless Inference for Large Language Models. OSDI 2024。
- Y. Sheng et al. S-LoRA: Serving Thousands of Concurrent LoRA Adapters. MLSys 2024。
- R. Prabhu et al. vAttention: Dynamic Memory Management for Serving LLMs without PagedAttention. 2024。
- D. Du et al. Catalyzer: Sub-millisecond Startup for Serverless Computing with Initialization-less Booting. ASPLOS 2020(AFaaS 的基础)。
背景
- 前置讲 P1:GPU 的结构与 roofline;前置讲 P3:虚拟内存、页表、调度(“内存气球”来自虚拟化)。
- 第 1 讲:Clockwork 的可预测性;第 3 讲:PagedAttention;第 11 讲:多副本路由。
12. 下一讲预告 · Next lecture
到这里,我们看的”请求”大多是彼此独立的一问一答。可现在越来越多的流量来自程序:一个 agent 为了完成一个任务,会连续调用 LLM 几十次,中间穿插工具调用;一个复合 AI 系统(第 11 讲的 LLMSelector)里,几个模块的调用有先后依赖,有的可以并行。如果推理系统只看到一个个孤立的请求,就会做出糟糕的决定:先来先服务会让一个很长的程序挡住许多短程序;不知道哪些调用在关键路径上,就没法优先处理它们;不知道几个调用共享同一段上下文,就错过了前缀缓存。第 13 讲:智能体工作流读两篇论文:Parrot 用”语义变量”把应用的数据流暴露给推理服务,据此做依赖感知的调度和前缀共享;Autellix 把”程序”当作调度的基本单位,按程序已经获得的服务量排队(类似操作系统里的”最少已获服务优先”),减少程序之间的排头阻塞。我们会在连续批处理引擎上模拟多调用的 agent 程序,比较按请求的先来先服务、按程序的最少已获服务优先,以及关键路径感知的调度。