Files
algo2025/greed/main.typ
2025-12-18 16:00:22 +08:00

203 lines
9.3 KiB
Typst
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#import "labtemplate.typ": *
#show: nudtlabpaper.with(
author1: "程景愉",
id1: "202302723005",
advisor: " 胡罡",
jobtitle: "教授",
lab: "306-707",
date: "2025.12.18",
header_str: "贪心算法分析实验报告",
minimal_cover: true,
)
#set page(header: [
#set par(spacing: 6pt)
#align(center)[#text(size: 11pt)[《算法设计与分析》实验报告]]
#v(-0.3em)
#line(length: 100%, stroke: (thickness: 1pt))
],)
#show heading: it => box(width: 100%)[
#v(0.50em)
#set text(font: hei)
#it.body
]
#outline(title: "目录",depth: 3, indent: 1em)
// #pagebreak()
#outline(
title: [图目录],
target: figure.where(kind: image),
)
#show heading: it => box(width: 100%)[
#v(0.50em)
#set text(font: hei)
#counter(heading).display()
#it.body
]
#set enum(indent: 0.5em,body-indent: 0.5em,)
#pagebreak()
= 实验介绍
#para[
贪心算法Greedy Algorithm是指在对问题求解时总是做出在当前看来是最好的选择。也就是说不从整体最优上加以考虑算法得到的是在某种意义上的局部最优解。多机调度问题是经典的 NP-Hard 问题本实验旨在通过实现和对比不同的贪心策略List Scheduling LPT深入理解贪心算法的近似比性质并探讨其在实际场景 GPU 集群调度)中的应用。
]
= 实验内容
#para[
本实验主要围绕多机调度问题的贪心算法展开,并扩展至在线 GPU 集群调度模拟。具体内容包括:
]
+ 实现两种贪心策略:任意顺序列表调度 (List Scheduling, LS) 最长处理时间优先 (Longest Processing Time, LPT)。
+ 实现基于分支限界 (Branch and Bound) 的最优解求解算法,作为性能评估的基准。
+ 构造特定的“最坏情况”输入,验证贪心算法的理论近似比下界。
+ 通过大量随机测试样本,统计不同算法的近似比分布及运行时间,分析 $m$ (机器数) $n$ (作业数) 对性能的影响。
+ (附加)模拟 GPU 集群在线调度场景,设计并对比不同的调度策略在不同负载下的表现。
= 实验要求
#para[
针对多机调度问题,实验具体要求如下:
]
+ 针对多机调度问题,实现 LS LPT 两种贪心算法。
+ 实现遍历的最优解求解算法(分支限界法)。
+ 构造最坏情况输入,结合理论证明进行讨论。
+ 固定 $m, n$,随机产生大量样本,计算贪心解与最优解的比值(近似比),并分析其概率分布。
+ 改变 $m, n$,对比分析结果。
+ 附加:模拟 GPU 集群调度,考虑利用率 $eta$ 和用户延迟 $delta$,设计多种策略并分析。
= 实验步骤
== 算法设计
=== 算法一:列表调度 (List Scheduling, LS)
#para[
LS 算法是最朴素的贪心策略。它按照作业输入的任意顺序,依次将作业分配给当前负载最小的机器。
该算法是一种在线算法,其时间复杂度为 $O(n log m)$ (使用优先队列维护机器负载) $O(n m)$ (线性扫描)。
理论上LS 算法的近似比为 $2 - 1/m$
]
```cpp
// 核心代码片段
long long greedy_ls(int m, const vector<Job>& jobs) {
vector<long long> machines(m, 0);
for (const auto& job : jobs) {
int min_idx = 0; // Find machine with min load
for (int i = 1; i < m; ++i) {
if (machines[i] < machines[min_idx]) min_idx = i;
}
machines[min_idx] += job.duration;
}
return *max_element(machines.begin(), machines.end());
}
```
=== 算法二:最长处理时间优先 (LPT)
#para[
LPT 算法在 LS 的基础上增加了预处理步骤:将所有作业按处理时间递减排序,然后依次分配给负载最小的机器。
排序操作使得较大的作业优先被处理,从而避免了最后剩下一个大作业导致机器负载极不均衡的情况。
该算法的时间复杂度主要由排序决定,为 $O(n log n)$
理论上LPT 算法的近似比为 $4/3 - 1/(3m)$
]
=== 算法三:最优解 (Branch and Bound)
#para[
为了评估贪心算法的性能,我们需要求得问题的最优解。由于多机调度是 NP-Complete 问题,我们采用深度优先搜索配合分支限界 (Branch and Bound) 来求解。
剪枝策略包括:
]
1. 当前最大负载已经超过已知最优解,停止搜索。
2. 理论下界剪枝:如果 `max(当前最大负载, (剩余作业总长 + 当前总负载)/m)` 超过已知最优解,停止搜索。
3. 对称性剪枝:若多台机器当前负载相同,则分配给它们是等价的,只尝试第一台。
== 最坏情况构造与分析
=== LS 算法最坏情况
#para[
*构造方法:* 对于 $m$ 台机器,输入 $m(m-1)$ 个时长为 1 的小作业,紧接着 1 个时长为 $m$ 的大作业。
]
#para[
*分析:* LS 算法会将前 $m(m-1)$ 个小作业均匀分配给 $m$ 台机器,每台机器负载为 $m-1$。最后的大作业将被分配给任意一台机器,使其最终负载变为 $(m-1) + m = 2m-1$
而最优解是将所有小作业均匀分配给 $m-1$ 台机器(每台负载 $m$),将大作业单独分配给剩下一台机器(负载 $m$),此时 MakeSpan $m$
近似比为 $(2m-1)/m = 2 - 1/m$
本实验通过代码验证了 $m=3, 4, 5$ 时的该情况,结果与理论完全一致。
]
=== LPT 算法最坏情况
#para[
*构造方法:* 经典的 LPT 最坏情况较为复杂,例如 $m=2$ 时,作业集为 $\{3, 3, 2, 2, 2\}$
]
#para[
*分析:* 排序后为 $3, 3, 2, 2, 2$
LPT 分配M1: $3, 2, 2$ (总 7), M2: $3, 2$ (总 5)。MakeSpan = 7。
最优解M1: $3, 3$ (总 6), M2: $2, 2, 2$ (总 6)。MakeSpan = 6。
近似比 $7/6 approx 1.167$。理论界 $4/3 - 1/6 = 7/6$。实验验证吻合。
]
== 实验数据与可视化
#para[
我们对 $m in {3, 5, 8}$ $n in {10, dots, 100}$ 进行了大量随机测试。
]
#figure(
image("results/ratio_boxplot.png", width: 80%),
caption: [LS LPT 算法近似比分布对比],
)
#figure(
image("results/ratio_vs_n.png", width: 80%),
caption: [近似比随作业数量 n 的变化趋势],
)
#figure(
image("results/time_comparison.png", width: 80%),
caption: [算法平均运行时间对比],
)
= 实验结果分析
#para[
1.*近似比性能:* 从箱线图可以看出LPT 算法的近似比极其接近 1通常在 1.0 - 1.05 之间性能极其优越且稳定。相比之下LS 算法的近似比分布较宽,平均在 1.1 - 1.3 之间,且随着 $m$ 的增加,最差情况(近似比上界)有升高的趋势,符合 $2 - 1/m$ 的理论预测。
]
#para[
2.*规模的影响:* 随着作业数 $n$ 的增加LS 的近似比往往会下降并趋于稳定。这是因为大量随机作业往往能“填平”机器间的负载差异。LPT 则始终保持高效。
]
#para[
3.*运行时间:* 贪心算法LS, LPT的运行时间极短微秒级且随 $n$ 线性或近线性增长。最优解算法B&B $n$ 指数级增长,当 $n > 20$ 时已难以在短时间内求解,验证了 NP-Hard 问题的计算复杂性。
]
= 实验总结
#para[
本实验深入分析了多机调度问题的贪心求解策略。实验结果表明,虽然 LS 算法实现简单但在最坏情况下性能较差。简单的排序预处理LPT 策略)能带来巨大的性能提升,使其在绝大多数随机及构造测试中都能获得极接近最优解的结果。这启示我们在设计贪心算法时,合理的贪心顺序(如优先处理“困难”或“大”的任务)至关重要。
]
#pagebreak()
= 附加GPU 集群在线调度模拟
== 场景描述
#para[
模拟一个拥有 $m=64$ GPU 的集群任务调度。任务到达服从泊松分布,单机执行时间服从均匀分布。任务支持并行 ($k$ GPU),但存在并行效率损耗:效率因子 $E_k = sigma^(log_2 k)$,其中 $sigma in [0.75, 0.95]$。系统目标是平衡 *集群利用率 ($eta$)* *用户平均延迟 ($delta$)*
]
== 调度策略设计
#para[
我们设计了三种策略进行对比:
]
+ *策略 A保守策略 (Conservative)* 总是为每个任务分配 $k=1$ GPU。其思路是最大化计算资源的“有效性”避免并行损耗。
+ *策略 B激进策略 (Aggressive)* 总是尽可能分配最大的并行度(如 $k=32$ $64$)。其思路是最小化单任务执行时间,但忽略了巨大的资源浪费。
+ *策略 C自适应策略 (Adaptive)* 根据当前等待队列的长度动态调整 $k$。若队列为空,使用高并行度加速;若队列拥堵,降低并行度以提高吞吐量。
== 模拟结果
#figure(
image("results/gpu_sim_plots.png", width: 90%),
caption: [不同负载下三种策略的利用率与延迟对比],
)
#para[
实验在轻负载 ($lambda=0.5$)、中负载 ($lambda=0.9$) 和重负载 ($lambda=1.1$) 下进行了模拟。结果显示:
]
+ *保守策略 (Conservative)*:在所有负载下都能保持较低的延迟。
+ *激进策略 (Aggressive)*:表现极差。由于并行效率损失,导致系统迅速过载,用户延迟呈爆炸式增长。
+ *自适应策略 (Adaptive)*:表现最为均衡。在轻负载时加速任务,在重负载时保证系统稳定性。
== 结论
#para[
在具有并行开销的资源调度场景中,盲目追求高并行度(激进策略)是不可取的。通过感知系统负载来动态调整资源分配粒度的 *自适应策略*,是更为优越的解决方案。
]