这一篇在干嘛?

论文《FC-GPU: Feedback Control GPU Scheduling for Real-time Embedded Systems》(Subramaniyan & Wang,ACM TECS 2025,25 页)要解决一个越来越普遍的问题:自动驾驶、VR 头显这类嵌入式系统把 GPU 当成关键算力,但 GPU 上的任务执行时间会剧烈波动,传统调度方法保证不了实时性。现有方案绝大多数是开环的,依赖离线估计的最坏执行时间(WCET),估计小了就超时、估计大了就浪费硬件。FC-GPU 是第一个把**反馈控制实时调度(FCS)**用到 GPU 上的工作:它为每个任务定义响应时间比 RTR = 响应时间 / 周期,推导出一个多输入多输出(MIMO)系统模型来描述任务之间的资源争抢,再基于该模型设计比例控制器,动态调每个任务的调用速率,把 RTR 稳定控制在设定点(如 0.9)。在 NVIDIA RTX 3090 和 AMD MI-100 上的实测表明,即使任务执行时间在运行中大幅增加,FC-GPU 也能把响应时间方差从基线的 4499.55 ms² 降到 0.45 ms²,执行抖动从 24.74% 降到 5.15%,截止错失率压到 2% 以下

一、问题:为什么 GPU 上的实时调度这么难

1.1 先说清楚”实时”在这里是什么意思

嵌入式系统里的”实时”(real-time)不等于”快”,而是**“必须在截止时间(deadline)之前完成”**。一个每秒必须处理 30 帧图像的自动驾驶感知任务,如果第 100 帧处理了 50 毫秒,哪怕平均只要 10 毫秒,这一帧也算是失败——因为车已经往前开了 1.5 米,决策用的是过期信息。

本文讨论的是软实时(soft real-time):偶尔错过截止可以容忍,但要尽可能少、尽可能可控。这与硬实时(一次都不能错)相对。自动驾驶、VR 渲染、视频监控都属于软实时——掉一帧人会难受,但不会立刻出人命。

1.2 GPU 的四个”不讲道理”

CPU 上的实时调度理论已经发展了几十年,有成熟的速率单调调度(RMS, Rate Monotonic Scheduling)和可调度利用率界(schedulable utilization bound)——比如”只要总利用率低于 69%,RMS 一定可调度”。但 GPU 上这套东西几乎全部失效。原因有四条,一条比一条致命:

(1)GPU 利用率没有可调度界。 CPU 上的 RMS 界是从”周期性任务 + 可抢占 + 利用率 = 执行时间/周期”这套模型推出来的。但 GPU 的执行时间本身会随并发任务数变化(下一节会看到,N 个任务并发时每个任务的响应时间约为 N × e_i),所以”利用率”这个分母就不固定,界自然无从谈起。这是为什么已有的反馈控制调度(FCS)不能直接搬到 GPU 上的第一条理由。

(2)GPU 内部调度不透明。 大部分商用 GPU 的硬件调度器是闭源的。你提交一个 kernel(核函数),它什么时候被放到哪个 SM(流式多处理器)上跑、什么时候被切走、中间被抢占了几次,你都看不见,只能通过 nvidia-smi 之类的工具看到很粗的状态。这意味着你没法像在 CPU 上那样精确计算一个任务的响应时间上限

(3)GPU 缺乏有效的抢占。 CPU 上任意指令边界都能做上下文切换。GPU 不一样:一个 kernel 一旦发射,往往要跑到某个较粗的粒度才能被打断(论文提到数据搬运是在分段边界处抢占)。GPU 上下文切换的开销主要来自保存巨大的寄存器文件和刷新缓存,文献报告一次 GPU 上下文切换要 50 到 750 微秒——这在毫秒级任务上是不可忽略的比例。

(4)任务之间会互相拖慢,而且是”我调我自己会害了别人”。 这是本文建模的核心。如果任务 A 提高调用速率,它占用 GPU 的时间片变多,任务 B 的响应时间就变长;B 的变长又会导致 B 的完成时间推迟,反过来影响 A。这是一种耦合。

1.3 开环方案的死穴:WCET 估不准

传统的实时 GPU 调度基本都是**开环(open-loop)**的:离线用基准程序测出每个任务的最坏执行时间 WCET(Worst-Case Execution Time),然后按这个 WCET 做可调度性分析,把参数写死,运行时不再改。

问题在于执行时间会剧烈变化。论文给了一个非常具体的例子:Apollo(百度开源自动驾驶平台)的交通灯检测器,执行时间会因为检测到的交通灯数量不同而增加 330%。也就是说,最坏情况是最坏情况的 4.3 倍。

于是系统设计者陷入两难:

  • 低估 WCET → 运行时出现截止错失(deadline miss);
  • 高估 WCET → 按最坏情况配置硬件,得多买好几块 GPU,成本爆炸。

论文明确指出:虽然保守地高估 WCET 确实能保证运行时可调度,但这会导致所需 CPU/GPU 数量显著增加,系统设计者通常不想要这个结果。

1.4 为什么反馈控制是对的解药:恒温器类比

到这里,思路其实已经很自然了。想想家里的恒温器(thermostat)

  • 你设定 20°C(设定点 set point);
  • 温度计测当前室温(测量 measured output);
  • 算误差 = 20 − 当前温度(误差 error);
  • 误差大就多烧一会儿暖气(控制输入 control input);
  • 隔一段时间重复一次(采样周期 sampling period)。

恒温器不需要知道房子的热容、墙的导热系数、外面刮几级风、窗户漏不漏气。它只看”现在冷不冷”,然后按比例调节。这就是反馈控制的威力:用一个粗略的模型 + 持续的测量修正,换来在模型不准甚至环境剧变时依然稳如老狗的行为。

FC-GPU 做的就是这件事,只不过:

恒温器FC-GPU
房间温度任务的响应时间比 RTR
设定温度 20°CRTR 设定点(如 0.9)
暖气功率任务的调用速率 r_i
温度计响应时间监视器(Response Time Monitor)
温控算法MIMO 比例控制器
暖气阀门任务速率执行器(Task Rate Actuator)

关键的设计洞见是:论文没有选择”控制 GPU 利用率”或”控制截止错失率 DMR”作为目标,而是选择了控制每个任务的响应时间。原因是:

  • 利用率不行:如上所述,RMS 界不适用于 GPU;
  • DMR 不行:已有的 FCS 工作把截止错失率控制在一个设定值(比如 1%),但恒定 1% 的错失率即使对软实时系统也是不可接受的——你总不能跟用户说”我们保证每 100 帧必定掉 1 帧”。

所以控制目标定为:让每个任务的响应时间都短于各自的截止时间。而做法不是直接改优先级或时间片长度(GPU 不给你这个接口),而是调任务的调用速率——跑慢一点,自然就不超时了。这个思路叫速率适配(rate adaptation),在实时系统里被广泛使用,而且很多真实系统本来就允许速率在一定范围内调整(实时多媒体、自动驾驶都是)。

二、前置知识:GPU 到底是怎么分时共享的

要设计控制器,必须先搞清楚被控对象的脾气。论文第 3 节专门分析了 NVIDIA 和 AMD 两种 GPU 的调度机制。

2.1 NVIDIA 的三种计算模式

NVIDIA GPU 有三种 compute mode:

模式行为本文是否采用
Exclusive(独占)同一时刻只有一个任务能访问 GPU 资源❌ 不采用——多任务场景下会让实时任务错过截止
Prohibited(禁止)没有任何 context 在 GPU 上执行❌ 不采用
Default(默认)用**时间分片(time-slicing)**共享✅ 采用,因为大多数 GPU 都这么做

设置为默认模式的命令是 sudo nvidia-smi -c 0

除了模式,还有一个关键要求:任务必须从不同的进程(process)启动。因为只有在设备驱动层面来自不同进程,每个进程才会被分配一个独立的 GPU context(代表 GPU 上的虚拟地址空间和运行时状态),来自不同进程的 context 才会被时间分片调度。如果所有任务都在同一个进程里发 kernel,它们会串行排队,根本拿不到并发。

2.2 AMD 需要改驱动

AMD GPU 默认使用空间共享(spatial sharing)——多个进程同时占用 GPU 的不同计算单元。为了统一到时间分片模型,论文修改了 amdgpu 驱动,把模块参数 hws_max_conc_proc 设为 1,把驱动从”多进程共享”切换到”单进程优先”模式,然后重新编译驱动。

这个细节很能说明问题:在真实 GPU 上做实时调度研究,光是让实验环境符合你的模型假设就得动驱动。

2.3 GPU 时间分片 ≠ CPU 时间分片

两者最大的区别在于上下文切换的代价和做法:

  • 开销来源:保存大量寄存器文件(register files)和刷新缓存(cache flushing)。GPU 有成千上万个寄存器,全存一遍代价极高。
  • 优化手段:GPU 采用部分状态保存(partial state-saving)——共享内存、寄存器、全局内存这些跨时间片保留,不做不必要的数据搬运。
  • 内存管理:从 Pascal 架构开始,NVIDIA GPU 使用按需分页(demand paging),这保证了一个 context 的 GPU 内存在上下文切换时不会被换出(无论集成还是独显)。
  • 数据搬运:GPU 用专门的**拷贝引擎(copy engine)**把数据从主存搬到显存,驱动会把大块传输切成小段,抢占只发生在这些分段的边界

2.4 最重要的经验规律:响应时间 ≈ N × e_i

论文用一个矩阵乘法负载做了实验:1 个进程、2 个、3 个、4 个同样的矩阵乘进程并发,测响应时间。结果符合一个非常干净的规律——响应时间正比于 N × e_i,其中 N 是并发任务数,e_i 是独占 GPU 时的执行时间。

为什么会这样?因为理想时间分片下,GPU 把时间平均分成 N 份,每个任务每轮只拿到 1/N,所以完成时间就变成 N 倍。这个观察与很多 NVIDIA GPU 使用的时间分片机制一致。

这条规律是整个建模的基础:它告诉我们,任务之间的争抢可以用”别人的有效执行时间 × 别人的调用速率”来量化——这正是下一节模型的来源。

三、控制目标:用 RTR 把”离截止还有多远”量化

3.1 任务模型

系统中有 N 个周期性任务 t_i(i = 1..N)。每个任务带这些参数:

符号含义
e_i任务在 GPU 上的执行时间
r_i(k)第 k 个采样周期内的任务速率(task rate)
θ_it_i 的上下文切换开销
ẽ_i = e_i + θ_i有效执行时间(effective execution time)
p_i(k) = 1 / r_i(k)第 k 个周期内的任务周期
d_i(k)t_i 的截止时间
j_ikt_i 在第 k 个周期内的第 k 个作业(job)
h_ik / f_ik作业 j_ik 的释放时间 / 完成时间
q_i(k) = f_ik − h_ik作业 j_ik 的响应时间(response time)
z_it_i 的 RTR 设定点
rtr_i(k)作业 j_ik 的响应时间比(RTR, Response Time Ratio)

两个关键假设:

  1. 所有任务都是周期性的。这在嵌入式系统里很合理——传感器、摄像头就是周期性交互的。
  2. 截止时间等于周期。因为如果一个作业在自己的周期内没跑完,下一个作业就被释放了,当前作业的结果已经过时。

3.2 RTR:本文的核心度量

响应时间比(RTR)定义为响应时间除以周期:

它的语义极其直观:

  • RTR = 1:作业恰好在截止时间完成;
  • RTR > 1错过截止
  • RTR < 1:提前完成。

举个论文里的例子:设定点 0.9、周期 2 秒,意味着每个作业要在 1.8 秒内完成。如果某个作业跑了 1.85 秒,它仍然满足 2 秒的截止时间,只是没达到设定点。

为什么留这 10% 的余量? 这是控制系统的经典做法——设定点留一段安全边界(safety margin),让系统有空间吸收扰动而不至于立刻越界。就像恒温器不会把设定点设成”温度上限”,而是设在中间。

3.3 形式化:一个带约束的优化问题

有了 RTR,控制问题就能写成一个约束优化:

其中优化变量是所有任务的速率向量:

约束条件:

目标函数的含义很直白——最小化所有任务的设定点与实际 RTR 之差的平方和。注意这里是所有任务一起最小化,不是各管各的,这一点后面会非常关键。

约束也很重要:任务速率不能无限调。太慢了系统失去意义(比如摄像头 1 秒才采一帧,自动驾驶就撞了),太快了硬件承受不了。所以速率只能在一个允许区间内动。

论文特别强调:这个优化问题不是离线解的,而是在线用闭环反馈机制解的。 传统做法是先建模再离线求最优解;FC-GPU 是”每个采样周期测一次状态、算一次误差、调一次速率”,让系统自己收敛到最优工作点。这种做法的优势是能立即对变化做出反应,并且在负载波动或执行时间变化时保持鲁棒。

四、FC-GPU 架构:一个跑在 CPU 上的控制环

4.1 整体框架

图 1:FC-GPU 控制回路总体框架。控制器周期性地监视每个任务的响应时间比(RTR),据此调整任务速率,使各任务的响应时间短于各自的截止时间。整个 MIMO 控制器位于宿主 CPU 上,由「响应时间监视器 → 控制器 → 任务速率执行器」三部分组成。

4.2 一个控制周期里发生的三件事

控制回路以固定周期被触发,每次做三步:

  1. 监视(Monitor):测量上一控制周期内每个任务的响应时间比 rtr_i(k)。
  2. 控制(Controller):收集所有 rtr_i(k) 以及输入的任务速率 r_i(k),计算出一个比例修正量 Δ(需要加到任务速率上的增量)。
  3. 执行(Actuator):任务速率执行器收到 Δ 后,据此调整所有周期性任务的 r_i(k)。

用控制理论的术语说:

  • 被控变量(controlled variable) = RTR,是我们想稳住的量;
  • 操纵变量(manipulated variable) = 任务速率 r_i,是控制器能动的旋钮;
  • 参考输入(reference) = 设定点 z_i。

这个类比一定要记牢:FC-GPU 唯一能动的旋钮就是”让任务跑多快”。它不能改 GPU 优先级(闭源驱动不给接口)、不能改时间片长度、不能抢占 kernel。能调的只有速率——这也决定了后面”执行(actuation)“那一步必须做一次换算。

4.3 为什么”速率适配”是个合理的旋钮

论文给出了三条理由:

  1. 广泛使用:速率适配在实时系统里是成熟手段;
  2. 真实系统本来就允许:实时多媒体应用、自动驾驶系统的任务速率本来就在一定范围内可调;
  3. 初始速率未必最优:即使离线给了一组初始速率,也未必能让系统达到期望的利用率和最小错失率,需要在运行时修正。

五、系统建模:把 GPU 变成一个能被控制的受控对象

这是全文最有技术含量的部分。目标:找到任务速率 r_i(k)(输入)与响应时间 q_i(k)(输出)之间的数学关系。

5.1 从实验观察出发

图 2:响应时间遵循 N × e_i 的规律(横轴为并发任务数,纵轴为响应时间)。随着并发任务数从 1 增加到 4,每个任务的响应时间近似线性增长为原来的 N 倍,这正是时间分片机制的直接后果。

论文用的实验负载是矩阵乘法:每个线程计算输出矩阵的一个元素(取 A 的一行与 B 的一列做点积),用 2D grid 与 2D thread block(32, 32)启动,每个 block 算一个 32×32 的输出块。为了优化内存传输,使用了 zero-copy pinned memory(零拷贝锁页内存),让 GPU 直接访问主机内存。响应时间用 CUDA 时间戳器测量。

5.2 响应时间 = 执行时间 + 争抢时间

实时系统里,任务的响应时间通常建模成两部分之和:

  • 执行时间 e_i:独占 GPU 时完成任务所需的时间。这个可以离线用基准程序估计
  • 争抢时间 c_i:多个任务竞争同一 GPU 资源时引入的额外延迟。这个无法离线预知

争抢从哪来?一是上下文切换,二是对 GPU 计算/带宽资源的争夺。关键规律是:一个任务越频繁地竞争 GPU 时间,其他协同调度任务的争抢时间就越高,从而拉长所有任务的响应时间。

5.3 两个具体例子

论文给了两个算例帮助理解,我们把它们完整走一遍。

图 3:例 1 中 t₁ 与 t₂ 各作业的释放时间(t₁(1, 2)、t₂(1.5, 3))。

例 1:t₁(1, 2) 和 t₂(1.5, 3)——即 t₁ 执行时间 1、周期 2;t₂ 执行时间 1.5、周期 3。取 t₁ 的 3 个作业和 t₂ 的 2 个作业。

  • 时刻 0:两个任务同时释放,j₁₁ 和 j₂₁ 被时间分片共享;
  • 时刻 2:j₁₁ 执行完毕,j₁₂ 立刻释放,继续与 j₂₁ 竞争;
  • 时刻 3:j₂₁ 跑完,j₂₂ 释放,与 j₁₂ 竞争、之后与 j₁₃ 竞争。

按 q_i = f_ik − h_ik 计算:j₁₁、j₁₂、j₁₃ 的响应时间都是 2;j₂₁、j₂₂ 的响应时间都是 3。平均响应时间分别是 2 秒和 3 秒。

图 4:例 2 中 t₁ 与 t₂ 各作业的释放时间(t₁(0.75, 2)、t₂(1, 2.5))。

例 2:t₁(0.75, 2) 和 t₂(1, 2.5)。

  • j₁₁ 与 j₂₁ 同时释放,时间分片;
  • j₁₁ 在时刻 1.5 完成,之后 j₂₁ 独占运行,在 1.75 完成;
  • 时刻 2:j₁₂ 释放,此时 GPU 空闲,独占全部资源,时刻 3 完成;
  • 时刻 2.5:j₂₂ 释放,与 j₁₂ 时间分片,最终在 3.75 完成;
  • j₁₃ 在时刻 4 独立释放,0.75 秒跑完。

响应时间:j₁₁ = 1.5、j₁₂ = 1、j₁₃ = 0.75(平均 1.08);j₂₁ = 1.75、j₂₂ = 1.25(平均 1.5)。

这两个例子想说明什么? 注意例 2 中 j₁₂ 的响应时间是 1,而 j₁₁ 是 1.5——同一个任务,不同的作业,响应时间可以差一倍,完全取决于当时有多少别的任务在抢。这种波动正是开环方案无法处理的东西。

5.4 推导出模型

现在把例子一般化。考虑两个任务 t₁ 和 t₂:

  • 如果 t₁ 的周期 p₁ = 1/r₁ 接近它的执行时间 e₁,且没有其他任务在用 GPU,那 t₁ 吃满所有 GPU 时间,响应时间 q₁ = e₁。
  • 当 t₂ 同时活跃时,两者在重叠区间内竞争 GPU 时间。r₂ 越大,t₂ 被调度的频率越高,重叠区间越频繁,竞争越激烈、上下文切换越频繁,t₁ 和 t₂ 的响应时间都被拉高
  • 反过来,r₂ 越小,t₁ 的响应时间越短。

核心结论:任务 t₁ 的响应时间,取决于别人的速率。 具体量化——t₂ 对 t₁ 造成的争抢,等于 t₂ 的”有效执行时间 × 调用速率”:

这个式子非常优雅,值得逐项读:

  • e_1:我自己独占时要跑的时间,这部分谁也拿不走;
  • (θ_2 + e_2):t₂ 每次出现要吃掉的 GPU 时间(执行 + 上下文切换);
  • r_2(k-1):t₂ 的调用速率,即”每秒出现几次”;
  • 三者相乘:别人每秒来抢我多少时间,就是我的争抢延迟。

注意 r_2(k-1)上一拍的速率——因为这一拍的速率还没生效,系统有一步延迟。这是离散控制系统的标准写法。

论文特别指出:文献报告 GPU 上下文切换耗时在 50 到 750 微秒之间,会显著增加任务响应时间。所以 θ 这一项不是可以忽略的高阶小量。

5.5 从”绝对值”到”增量”:差分方程

控制器设计需要的是”输入变化一点点,输出会变多少”,也就是增量形式。对 t₁ 把上一拍的方程写出来:

两式相减:

定义速率增量:

代入得到单个任务的差分方程:

这个转换是控制理论里的标准操作:常数项 e₁ 被消掉了,方程变成了”输出增量 = 系数 × 输入增量”,控制器设计起来容易得多,而且不再依赖 e₁ 的精确值——这正好呼应了”反馈控制不需要精确模型”的优点。

两个任务的联立形式:

其中 ẽ_i = e_i + θ_i 是有效执行时间。

5.6 推广到 N 个任务:MIMO 矩阵形式

对 N 个任务,t₁ 受到的争抢是所有其他任务的争抢之和:

写成增量形式:

最后是标准的 MIMO(多输入多输出)形式:

矩阵 A 的结构是这个模型最值得记住的地方

  • 对角线 A_ii = 0——一个任务的速率不直接影响自己的响应时间(只通过影响别人间接影响);
  • 非对角线 A_ij = ẽ_j(i ≠ j)——任务 j 的速率直接影响任务 i 的响应时间。

展开成完整矩阵:

这条对角线为零、非对角线非零的结构,就是”必须用 MIMO 控制器”的全部理由。 如果 A 是对角阵(各任务互不影响),那 N 个独立的单回路(SISO)控制器就够了;但 A 的非对角项不为零,说明动一个任务会牵动所有其他任务,各管各的控制器一定会互相打架。

六、控制器设计与稳定性分析

此文件夹下有0条笔记。