深夜的网红餐厅里,主厨长阿理站在出菜口,眼前堆着30张订单,身后是10位厨师和8个炉灶。如果他把所有订单都塞给技术最好的张师傅,张师傅会忙到手忙脚乱,实习生小李却在一旁刷手机;如果平均分下去,快厨师很快干完闲逛,慢厨师越积越多。怎么分单才能让每个灶台火力全开、客人上菜最快?这正是“多任务并行调度与负载均衡”要回答的问题。在AI世界里,GPU上的每个计算核心就像一位厨师,模型里的每个算子就像一道菜,而TaskScheduler就是那位掌握全局的主厨长。它的分配策略,直接决定算力有没有被榨干。这篇文章,我们跟着阿理一边管后厨,一边把调度器的门道一一拆解。

岗位关联

掌握TaskScheduler的负载均衡策略,是AI编译器工程师优化算子执行效率、压榨硬件算力的核心技能,也是衡量系统性能调优能力的一道分水岭。

目录
  1. 可视化演示
  2. 调度器核心职责
  3. 负载均衡度量
  4. 优先级调度
  5. 资源分配策略
  6. 协同依赖管理
  7. 性能调优实战
  8. 工程落地要点
  9. 典型坑与避坑
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

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

任务优先级与资源分配 优先级队列 (Weighted Priority) 加权优先级公式 + w3*critical_path Score = w1*priority + w2*urgency T_total = T_compute + N*T_overhead</span...

调度器核心职责

阿理当上主厨长的第一天,发现出菜口乱成一团:订单堆得像小山,有的菜做了三遍,有的菜压根没人做。他立刻做了三件事:先把订单全部收拢成一摞(收集任务),再按热菜、凉菜、甜品分类(任务分类),最后在每张订单上贴三个小标签——用什么锅、客人急不急、哪道菜要等哪道菜(需求、优先级、依赖)。分好类,他才把订单交给厨师。

在AI编译器的自动调度器中,TaskScheduler干的正是这三件事。它接收上游分析阶段产出的算子列表,把它们包装成带有完整元信息的任务对象,放进一个“任务队列”——也就是那张订单传送带。随后,调度循环不断从队列中取出“可以被执行”的任务,绑定到GPU或CPU的空闲执行单元上运行。

每个任务携带的元信息丰富到什么程度?包括算子形状(Shape)、数据类型、估计的浮点计算量、内存占用、依赖的前驱算子ID、优先级标记等。调度器通过这些元信息判断任务是否就绪、是否需要迁移数据,以及放在哪个执行单元上最快。整个过程反复循环,直到计算图中的所有任务都被调度完毕。

阿理看着传送带上的订单想明白了:好的调度不是碰运气,而是把任务的“身份信息”记录清楚,再有序地分发。任务队列就是那张传送带,而元信息就是订单上的标签——标签写得越细,分发越有底气。

Q={T1,T2,…,Tm},Tj=(需求j,优先级j,依赖j)Q = \{T_1, T_2, \ldots, T_m\}, \quad T_j = (\text{需求}_j, \text{优先级}_j, \text{依赖}_j)
这个公式表示:调度器维护一个任务集合 $Q$,其中每个任务 $T_j$ 都附带三个关键字段——需求、优先级、依赖。它是后面所有调度策略的“原材料”。
  1. 任务集合用一个花括号把 m 个任务收拢成一张待办清单。
    Q={T1,T2,…,Tm}Q = \{T_1, T_2, \ldots, T_m\}
  2. 任务结构每个任务 $T_j$ 由需求、优先级、依赖三项元信息描述。
    Tj=(需求j,优先级j,依赖j)T_j = (\text{需求}_j, \text{优先级}_j, \text{依赖}_j)
  3. 需求字段记录算子类型、输入形状、内存占用等“做这道菜要什么锅”。
    需求j=(算子类型,形状,内存)\text{需求}_j = (\text{算子类型}, \text{形状}, \text{内存})

负载均衡度量

经营一周后,阿理发现问题:技术好的张师傅面前总排着长龙,新来的小李门可罗雀。阿理想了个笨办法——“轮询”:订单按顺序轮流发给张师傅、李师傅、王师傅、陈师傅。可这样真的公平吗?张师傅做得快,很快做完又闲着;小李做得慢,积压越来越多。平均分单并没有带来均衡。

轮询策略(Round-Robin)在操作系统和硬件调度中非常常见:任务编号对工作人员数取模,机械地轮流分配。它的优点是简单、无状态、不用监测任何负载;但缺点也很明显——它完全不考虑每个执行单元的实时积压。于是有了更聪明的“最短队列优先”策略:新任务到达时,调度器查看每个 Worker 当前的待办队列长度 $q_i(t)$,选择队列最短的那个。

为了量化负载是否均衡,调度器还引入了“不平衡度”指标 $I(t)$,也就是最忙 Worker 与最闲 Worker 的队列长度之差。$I$ 越接近 0,说明负载越均匀;$I$ 越大,说明有人在累死、有人在闲死。

阿理在后厨挂了一块白板,实时更新每位厨师的待办数,谁的积压最少新订单就给谁。这块白板,就是调度器心里的“负载画像”——负载均衡不是平均分单,而是让每个人手头的活差不多重。

i∗=arg⁡min⁡iqi(t),I(t)=max⁡iqi(t)−min⁡iqi(t)i^* = \arg\min_{i} q_i(t), \qquad I(t) = \max_{i} q_i(t) - \min_{i} q_i(t)
左边公式是“谁最闲就派给谁”的选择逻辑;右边公式是衡量“忙闲差距”的标尺。
  1. 最短队列选择在 n 个 Worker 中选出队列长度最小的那个。
    i∗=arg⁡min⁡iqi(t)i^* = \arg\min_{i} q_i(t)
  2. 队列长度$q_i(t)$ 表示第 i 个 Worker 在 t 时刻的待办任务数。
    qi(t)∈Nq_i(t) \in \mathbb{N}
  3. 不平衡度最忙的减去最闲的,差值越小越均衡。
    I(t)=max⁡iqi(t)−min⁡iqi(t)I(t) = \max_{i} q_i(t) - \min_{i} q_i(t)

优先级调度

餐厅里常有VIP客人打电话催单:“我那桌是生日宴,蛋糕先上。”阿理就会在蛋糕订单上贴个“加急”标签。可没过几天,他发现有张普通菜单已经等了一个小时还没人做——客人急得拍桌子。阿理这才意识到,如果只认加急标签,普通客人永远上不了菜。

TaskScheduler 中的任务也一样有优先级之分。任务创建时会被赋予一个“静态优先级” $P_{\text{static}}$,通常由编译优化阶段根据算子关键程度或关键路径长度设定。高优先级任务确实应该被优先调度,但如果只认静态优先级,低优先级的任务可能长期得不到执行,这就是“饥饿”(Starvation)——就像普通客人永远等不到位子。

解决饥饿的办法是加入“动态补偿”:任务等待的时间越久,它的有效优先级越高。最终用于排序的是“有效优先级” $P_{\text{eff}}(t)$ 等于静态优先级加上等待补偿。其中参数 $\alpha$ 控制补偿的强度:太大,普通任务会喧宾夺主;太小,又压不住饥饿。

阿理后来把规则改成了“VIP 加急,但普通菜等太久自动升级”。他笑称:优先级不是一张死标签,而是“重要度 + 耐心”的合体。调度器里没有绝对的高贵,只有让所有任务都有机会被执行的平衡艺术。

Peff(t)=Pstatic+α⋅Twait(t)P_{\text{eff}}(t) = P_{\text{static}} + \alpha \cdot T_{\text{wait}}(t)
有效优先级 = 天生的地位 + 等待带来的同情分。这两个因素共同决定谁先被调度。
  1. 有效优先级调度器真正用来排序任务的分数,随时间变化。
    Peff(t)P_{\text{eff}}(t)
  2. 静态优先级任务出生时就带有的重要度评分,不随时间改变。
    PstaticP_{\text{static}}
  3. 等待补偿项等待时间乘以补偿系数,等得越久分数越高。
    α⋅Twait(t)\alpha \cdot T_{\text{wait}}(t)

资源分配策略

餐厅只有8个炉灶,10位厨师。阿理不能给每人开一个灶,只能精打细算地分配火力:炖菜给小火慢灶,爆炒给大火猛灶。有的厨师技术好能同时管两口锅,实习生就只能一口锅炒一勺菜。灶台有限,如何分配成了效率的关键。

资源分配的核心是“比例配额”。每个 Worker 或任务有一个权重 $w_i$,代表它的能力或需求大小。调度器先算出所有权重之和,再按比例把总资源分给各方。这里的 $R_{\text{total}}$ 可以是计算核数量、缓存容量、显存带宽等。权重越大的任务,拿到的资源份额越多。

并行粒度的选择同样关键:任务分得太粗,一个大算子占满整个执行单元,其他单元只能干瞪眼;分得太细,调度和通信的开销反而吃掉收益。实战中,调度器会根据数据和算子的规模计算合理的分块大小,让每个 Worker 既有活干、又不至于撑爆。

阿理感慨:炉灶的火不是平均开的,而是按菜和厨师的能力动态调节。资源分配的智慧,从来不是简单的一刀切——让合适的任务拿到合适的资源,才叫真正的公平。

ϕi=wi∑j=1nwj,Ri=ϕi×Rtotal\phi_i = \frac{w_i}{\sum_{j=1}^{n} w_j}, \qquad R_i = \phi_i \times R_{\text{total}}
先算每个 Worker 的权重占比 $\phi_i$,再用占比乘以总资源,就得到它应得的那份资源。
  1. 权重比例第 i 个 Worker 的权重在总权重中的占比。
    ϕi=wi∑j=1nwj\phi_i = \frac{w_i}{\sum_{j=1}^{n} w_j}
  2. 单项权重$w_i$ 是第 i 个 Worker 的能力标度,值越大代表能承担越多。
    wi>0w_i > 0
  3. 归一化分母把所有人的权重加起来,保证各占比之和为 1。
    ∑j=1nwj\sum_{j=1}^{n} w_j
  4. 实际资源用占比乘以总资源,得到分配结果。
    Ri=ϕi×RtotalR_i = \phi_i \times R_{\text{total}}

协同依赖管理

有天客人点了“先腌后炸”的脆皮鸡,阿理特意嘱咐:必须等腌料入味才能下锅。可新来的小工不懂,直接下锅炸,结果皮不脆、肉没味。阿理明白,厨房里很多菜是有先后工序的,顺序错了就全乱套。

AI计算图同样有严格的依赖关系:卷积层的输出是激活层的输入,Pooling 算子依赖前面的特征图。TaskScheduler 在调度前会解析算子之间的依赖,建立一张有向无环图(DAG)。一个任务只有等所有前驱任务都完成后,才会进入“就绪队列”——也就是那盘待下锅的配菜。

决定任务开始时间的,是所有前驱中最晚完成的那一个。若最慢的前驱耗时过长,整条链都被拖住。因此调度器通常会优先调度“关键路径”上的任务,把稀缺资源向它们倾斜,从而尽可能压缩整体耗时。

阿理把这张依赖图画在厨房墙上:谁先谁后一目了然。他说,工序就是厨房的法律——腌料没入味不能下锅,前驱没完成不能执行。懂得等待,才能高效。

tstart(Tj)=max⁡Tk∈pred(Tj)tfinish(Tk),tfinish(Tj)=tstart(Tj)+djt_{\text{start}}(T_j) = \max_{T_k \in \text{pred}(T_j)} t_{\text{finish}}(T_k), \qquad t_{\text{finish}}(T_j) = t_{\text{start}}(T_j) + d_j
开始时间 = 所有前驱完成时间中最晚的那个;完成时间 = 开始时间 + 自己的执行时长。这就是依赖调度的基本算法。
  1. 前驱集合$\text{pred}(T_j)$ 是任务 $T_j$ 的所有前驱任务。
    pred(Tj)\text{pred}(T_j)
  2. 最晚完成时刻所有前驱中完成得最晚的那个时刻决定我何时能开始。
    max⁡Tk∈pred(Tj)tfinish(Tk)\max_{T_k \in \text{pred}(T_j)} t_{\text{finish}}(T_k)
  3. 任务时长$d_j$ 表示任务 $T_j$ 自己在执行单元上跑多少时间。
    djd_j
  4. 完成时刻开始时间加上自己的时长就是完成时间。
    tfinish(Tj)=tstart(Tj)+djt_{\text{finish}}(T_j) = t_{\text{start}}(T_j) + d_j

性能调优实战

周末晚高峰,阿理发现张师傅忙得冒烟,小李却频频发呆。他掏出秒表掐了一天,发现有的厨师8小时里真正炒菜的时间只有3小时。这个“忙闲比”实在太低了。阿理决定每天记录每位厨师的出菜数和空闲时间。

在系统性能分析里,这对应着“利用率” $U$,即忙时间除以总时间。利用率长期接近 100% 的是热点瓶颈,利用率过低则说明资源闲置。TaskScheduler 会持续监测每个执行单元的利用率:当某个 Worker 接近满载,调度器就把新任务派给别的 Worker;当整体利用率偏低,就提高并行度或合并小任务。

负载阈值设置也很关键:比如积压超过 3 个任务就暂停向该 Worker 派新单,积压低于 1 个任务就优先派单。阈值太小,调度太频繁,白白增加开销;阈值太大,又不能及时响应负载变化。更高级的“自调优”策略会结合历史记录动态调整阈值,让利用率、调度开销和任务完成时间三者达到平衡。

阿理把白板升级成“后厨仪表盘”:实时显示每位厨师的负荷率和积压数。他扫一眼仪表盘,就知道该让谁休息、该给谁派单。性能调优不是拍脑袋,而是让数据告诉你怎么做。

U=TbusyTbusy+TidleU = \frac{T_{\text{busy}}}{T_{\text{busy}} + T_{\text{idle}}}
利用率 = 真正干活的时间 ÷ 总时间。它是判断 Worker 是否被充分使用的最直观指标。
  1. 利用率$U$ 表示一个执行单元的忙碌程度,取值在 0 到 1 之间。
    UU
  2. 忙时间$T_{\text{busy}}$ 是真正在执行任务上的时间。
    TbusyT_{\text{busy}}
  3. 总时间忙时间加上空闲时间,就是观察周期的总时长。
    Tbusy+TidleT_{\text{busy}} + T_{\text{idle}}

工程落地要点

阿理的餐厅越开越大,光靠他肉眼盯白板已经不现实。他总结出“经营三件套”:第一,策略要能换——忙碌时用激进的分单法,空闲时换成省人力模式;第二,数据要看报表——每位厨师的出菜数、积压数都要记录;第三,问题要能追账——哪天出菜慢了,能回溯是哪一环卡住。

TaskScheduler 的工程实现同样面对这三大挑战。“策略热插拔”意味着调度算法可以在运行时切换——比如从“最短队列”切换到“带宽感知调度”,不需要重启程序。监控反馈则依赖异步的 Profiling 模块,以固定周期采集执行单元负载、任务完成时长等指标,喂给调度器做动态决策。

日志追踪让调度过程可回放:每个任务被谁调度、何时开始、何时结束、是否迁移,都会写入日志。有了这些记录,下次负载波动时可以快速定位瓶颈。但别忘了,调度本身有开销:决策“这个任务给谁”要花时间,保存当前任务、恢复下一个任务的“上下文切换”也要花时间。工程上常用分批调度、事件驱动等方式降低调度频率。

这三件套听着简单,做起来却需要精心设计。但它们是调度系统从“能跑”走向“好维护”的关键——能换菜单、能看报表、能查账单,才是成熟的调度系统。

Δtsched=tassign+tswitch\Delta t_{\text{sched}} = t_{\text{assign}} + t_{\text{switch}}
一次完整调度的时间 = 决策时间 + 切换时间。这个值越小,调度的性价比越高。
  1. 调度开销$\Delta t_{\text{sched}}$ 表示完成一次任务分配所付出的总时间成本。
    Δtsched\Delta t_{\text{sched}}
  2. 决策时间$t_{\text{assign}}$ 是调度器计算“任务给谁”所需的计算时间。
    tassignt_{\text{assign}}
  3. 切换时间$t_{\text{switch}}$ 是保存旧任务现场、载入新任务现场的时间。
    tswitcht_{\text{switch}}

典型坑与避坑

阿理踩过一个坑:为了追求均衡,他把一道菜切成几十个小步骤分给所有厨师,结果厨师们光交接就花了半小时,菜还炒糊了。另一位厨师说:“阿理,你把简单事搞复杂了!”这就是“过度调度”——任务被拆得太细,调度和切换的开销盖过了并行本身的收益。

类似的坑还有两个:其一是“伪共享”,多个计算核心频繁读写同一块缓存行,表面在并行,实际在互相等锁,就像两位厨师抢同一把菜刀;其二是“资源碎片”,任务被调度得七零八落,内存被切成碎片,大任务找不到连续的空间安身。这些都是并行系统特有的坑。

Amdahl 定律给所有并行调度敲响警钟:一个程序中无论加多少并行单元,加速比都受限于不可并行的串行部分。其中 $f$ 是可并行比例,$p$ 是并行单元数。当 $p$ 趋于无穷时,加速比的上限逼近 $1/(1-f)$——就算 99% 都能并行,最多也只能快 100 倍。

阿理把这三条教训写在厨房门口:别切太碎、别抢菜刀、别指望人多就能无限快。调度不是越复杂越好,真正的智慧是克制。

S(p)=1(1−f)+fpS(p) = \frac{1}{(1 - f) + \dfrac{f}{p}}
加速比 = 1 ÷(串行部分 + 并行部分分摊到 p 个单元后的耗时)。串行部分是永远绕不开的天花板。
  1. 加速比$S(p)$ 表示用 p 个单元并行比用 1 个单元快了多少倍。
    S(p)S(p)
  2. 可并行比例$f$ 是任务中能被并行执行的时间占比。
    0≤f≤10 \le f \le 1
  3. 串行比例$1 - f$ 是必须顺序执行的时间占比,无法通过加核缩短。
    1−f1 - f
  4. 并行分摊$\frac{f}{p}$ 表示原本并行的部分被 p 个单元分摊后的耗时。
    fp\frac{f}{p}

AI 应用场景

深度学习编译器的自动调优任务调度

阶段:模型部署与算子自动调优

解决的问题:一个模型里有几百个算子,每个算子有巨大的调优空间。如果串行跑调优任务,可能要花几天;TaskScheduler 把每个算子的候选实现当作“订单”,把多核 CPU 当作“厨师”,用最短队列策略并行分发,让所有核心都保持忙碌。

具体案例:在 TVM/Ansor 中,TaskScheduler 自动把算子调优任务分配给多个 CPU/GPU 执行单元,每个执行单元跑不同配置的 benchmark。调度器根据历史耗时动态调整分配,把容易卡住的算子优先派给空闲核心,最终将调优时间从数天压缩到几小时。

在线推理服务的负载均衡

阶段:模型上线后的在线推理

解决的问题:推荐系统模型上线后,线上推理请求像雪片一样飞来,必须快速分发到多块 GPU 上。如果按轮询硬分,慢的 GPU 会堆积请求,导致 P99 延迟飙升;用最短队列策略后,新请求永远进入排队最少的 GPU 实例,整体吞吐明显提升。

具体案例:某广告推荐系统的推理集群有 16 块 GPU,请求调度器每秒分发上万次推理请求。引入基于队列长度的负载均衡后,GPU 利用率从 55% 提升到 82%,P99 延迟下降 40%。

大规模分布式训练的任务协同

阶段:大规模模型训练

解决的问题:多机多卡训练时,数据分片、梯度同步、Checkpoint 保存等任务需要协同调度。若某张卡网络带宽低还被分到大量通信任务,就会成为拖后腿的“慢厨师”。依赖管理与负载均衡结合,可以保证关键路径不被慢节点卡死。

具体案例:千卡级大模型训练中,调度器把 AllReduce 通信任务和本地计算任务排成依赖图,优先调度关键路径上的任务,并为高带宽节点分配更多梯度聚合任务,训练吞吐提升近 20%。

Python 演示

python46 行
class TaskScheduler:    """模拟多任务并行调度中的负载均衡策略"""     def __init__(self, num_workers, processing_speeds):        self.num_workers = num_workers        self.speeds = processing_speeds        self.queues = [0] * num_workers        self.total_time = 0     def assign_round_robin(self, task_id):        worker = task_id % self.num_workers        self.queues[worker] += 1     def assign_shortest_queue(self):        worker = min(range(self.num_workers), key=lambda i: self.queues[i])        self.queues[worker] += 1        return worker     def step(self):        self.total_time += 1        for i in range(self.num_workers):            processed = min(self.queues[i], self.speeds[i])            self.queues[i] -= processed     def imbalance(self):        return max(self.queues) - min(self.queues)     @staticmethod    def simulate(num_workers, speeds, num_tasks, use_shortest):        sched = TaskScheduler(num_workers, speeds)        for t in range(num_tasks):            if use_shortest:                sched.assign_shortest_queue()            else:                sched.assign_round_robin(t)            sched.step()        return sched  if __name__ == "__main__":    SPEEDS = [3, 2, 1, 2]    for name, use_shortest in [("轮询策略", False), ("最短队列策略", True)]:        sched = TaskScheduler.simulate(4, SPEEDS, 300, use_shortest)        print(f"=== {name} ===")        print("各Worker剩余任务:", sched.queues)        print("负载不平衡度:", sched.imbalance())
目的:用 Python 模拟 4 个处理速度不同的 Worker,分别用轮询策略和最短队列策略分配 300 个任务,对比最终负载不平衡度。
  1. self.queues = [0] * num_workers初始化每个 Worker 的待办队列为 0,相当于白板上每位厨师的初始积压数。
  2. worker = task_id % self.num_workers轮询策略的精髓:用取模运算把任务依次轮流分发。
  3. worker = min(range(self.num_workers), key=lambda i: self.queues[i])最短队列策略的核心:用 min 函数找出积压最少的 Worker。
  4. processed = min(self.queues[i], self.speeds[i])模拟不同 Worker 的处理能力,速度快的一步能处理更多任务。
  5. return max(self.queues) - min(self.queues)返回不平衡度,运行结果中这个数字越小说明负载越均衡。

常见误区

误区总以为把任务拆得越细、并行度拉得越高,程序就跑得越快。于是把一个大算子切成几百个小碎片,分给所有执行单元。

正解调度本身有成本。任务被切碎后,调度决策、上下文切换、数据通信的开销会快速上涨,甚至超过并行带来的收益。就像阿理把一道菜切成几十个小步骤,厨师光交接就花了半小时。用 Amdahl 定律看,串行比例决定加速上限,盲目加并行度只会让 $S(p)$ 逼近天花板后停在原地。

为何常见:“人多力量大”的生活直觉太强,人们容易忽略并行系统里“拆分配对”也是要付钱的。

误区只要把任务轮流发给不同 Worker,就认为负载一定均衡,从不去看每个 Worker 的实时积压。

正解轮询只保证“数量均分”,不保证“负载均分”。每个 Worker 的计算速度、当前积压都不同,就像把订单轮流发给快厨师和慢厨师,快厨师闲得擦盘子,慢厨师堆成小山。真正的负载均衡要参考队列长度和真实处理能力,用最短队列这样的策略去磨平差距。

为何常见:轮询实现太简单、直觉上“公平”,而且任务同质且速度一致时表现确实不错,容易让人产生它是万能方案的错觉。

误区把所有重要任务的优先级都设成最高,认为这样就能最快完成所有关键计算。

正解如果所有任务都是最高优先级,优先级就失去了区分度;更危险的是,低优先级任务可能永远轮不到执行,发生“饥饿”。应该让静态优先级与动态等待补偿结合,用 $P_{\text{eff}}(t) = P_{\text{static}} + \alpha T_{\text{wait}}$ 让等待时间也参与发言。

为何常见:静态优先级直观好理解,人们容易忽略调度系统是长期运行的系统,极端情况下的公平性同样重要。

误区看到监控面板上所有 Worker 的使用率都是 80% 以上,就认为负载很均衡、系统很健康。

正解全员忙碌可能是“伪均衡”——比如慢 Worker 满负荷运行,快 Worker 却只用了一半算力,它们的“忙”并不等价。真正要看的是每个 Worker 的排队长度和任务处理速度是否匹配。此外,伪共享、资源竞争还会让忙碌变得无效。均衡的目标是让整体吞吐最优,而不是让大家一样忙。

为何常见:“都在干活”这个画面给人一种安全感,但监控指标不会告诉你这种忙碌是否有效率。

面试题

Q1. TaskScheduler 的任务队列中,任务 $T_j$ 包含哪三类关键信息?

单选题 · easy

  • A.算子名称、数据类型、代码行数
  • B.资源需求、优先级、依赖关系← 正确答案
  • C.内存地址、线程ID、进程编号
  • D.硬件型号、驱动版本、运行时长

答案:B。任务 $T_j$ 的三要素是需求(资源需求)、优先级(调度排序依据)、依赖(前驱关系),对应厨房订单上的“用什么锅、急不急、等哪道菜”。A、C、D 都是干扰项,虽然有些字段在工程中会存在,但不属于任务结构的三要素。

Q2. 轮询调度(Round-Robin)最大的缺点是什么?

单选题 · mid

  • A.实现复杂度太高
  • B.不考虑各Worker积压差异← 正确答案
  • C.需要额外的硬件支持
  • D.只能用于单核场景

答案:B。轮询的优点是简单、无状态,但它只是机械地轮流分配,完全不看每个 Worker 的实时负载。好比把订单轮流发给快厨师和慢厨师,快的空转、慢的堆积。A、C、D 都与事实相反:轮询实现很简单,不需要特殊硬件,也常用于多核并行。

Q3. 某调度器中 4 个 Worker 的队列长度分别为 [5, 2, 4, 1],此时负载不平衡度 $I$ 是多少?

单选题 · mid

  • A.3
  • B.4← 正确答案
  • C.5
  • D.6

答案:B。不平衡度 $I = \max q_i - \min q_i$,最忙的 Worker 队列是 5,最闲的是 1,两者之差为 4。这对应阿理白板上最忙厨师与最闲厨师的积压差,差值越大说明忙闲越悬殊。

Q4. 关于动态优先级(等待补偿)的作用,下列说法正确的有?

多选题 · mid

  • A.避免低优先级任务长期得不到执行← 正确答案
  • B.等待越久的任务优先级越高← 正确答案
  • C.可以直接替换静态优先级
  • D.使得高优任务永不排队

答案:AB。动态优先级通过 $P_{\text{eff}}(t) = P_{\text{static}} + \alpha T_{\text{wait}}$ 给等待任务“加分”,防止饥饿。A、B 正确。C 错误,动态补偿和静态优先级是组合关系,不是替换;D 错误,高优任务同样会在队列中等待,只是排得更靠前。

Q5. 资源分配策略中,权重 $w_i$ 的含义是什么?

单选题 · easy

  • A.任务的优先级大小
  • B.Worker的能力权重或重要度← 正确答案
  • C.队列中的任务数量
  • D.网络传输的带宽

答案:B。$w_i$ 表示第 i 个 Worker 的能力标度,权重越大,分到的资源比例越高。它和队列长度无关,也和网络带宽无关。在厨房场景里,$w_i$ 就像厨师的“掌勺能力分”,能力强的厨师可以也多分一些炉灶火力。

Q6. 在依赖管理中,任务 $T_j$ 可以开始执行的时刻是?

单选题 · mid

  • A.所有前驱任务完成的最晚时刻← 正确答案
  • B.所有前驱任务完成的最早时刻
  • C.队列中最多任务完成之后
  • D.优先级最高的任务完成之后

答案:A。$t_{\text{start}}(T_j) = \max t_{\text{finish}}(T_k)$,必须等最慢的前驱完成才能开工。这就像脆皮鸡必须等腌料入味才能下锅——腌制时间取决于最慢的那道工序,而不是最快的。

Q7. 根据 Amdahl 定律,若可并行比例 $f=0.8$,并行单元数 $p=4$,加速比 $S(p)$ 是多少?

单选题 · hard

  • A.2.5← 正确答案
  • B.4.0
  • C.3.2
  • D.1.6

答案:A。代入 $S(p) = 1/((1-f) + f/p)$,得 $S(4) = 1/(0.2 + 0.8/4) = 1/(0.2+0.2) = 2.5$。即 4 个并行单元只能带来 2.5 倍加速,因为它受到 20% 串行部分的限制。这也解释了为什么盲目加并行单元收益递减。

Q8. 过度调度会带来哪些典型问题?

多选题 · mid

  • A.上下文切换开销过大← 正确答案
  • B.调度开销超过执行收益← 正确答案
  • C.任务总是执行得更快
  • D.资源利用率反而下降← 正确答案

答案:ABD。任务切得太碎时,调度器要频繁做决策、频繁切换上下文,这些开销吞掉了并行收益,资源利用率不升反降。A、B、D 正确。C 恰恰相反,过度调度通常会让任务变慢而不是变快。

Q9. 公式 $P_{\text{eff}}(t) = P_{\text{static}} + \alpha \cdot T_{\text{wait}}(t)$ 中,$\alpha \cdot T_{\text{wait}}$ 的设计意图是什么?

单选题 · hard

  • A.缩短任务总执行时间
  • B.补偿任务等待时间防止饥饿← 正确答案
  • C.提高任务资源占用率
  • D.降低调度器复杂度

答案:B。等待补偿项让排队越久的任务优先级越高,从而避免低优先级任务被饿死。它不改变任务本身的执行时长,也不提高资源占用,反而让调度逻辑更丰富而非更简单。就好比普通客人等太久也会被“免排队升级”。

Q10. 关于“伪共享”(False Sharing),下列说法正确的有?

多选题 · hard

  • A.多个核频繁写入同一个缓存行← 正确答案
  • B.会引发缓存一致性同步开销← 正确答案
  • C.与任务调度完全无关
  • D.可通过填充对齐或数据隔离缓解← 正确答案

答案:ABD。伪共享发生在多个计算核心各自操作不同变量、但这些变量恰好落在同一缓存行时,缓存一致性协议会让它们互相等待。A、B 正确。它和任务调度息息相关——调度器把哪些任务放到相邻核心会影响缓存行冲突。D 正确,通过内存填充对齐可以让变量分散到不同缓存行。