简介:这是一份北京化工大学本科毕业设计《基于深度强化学习的云工作流调度》项目源码包,面向计算机、人工智能及相关专业学生,可作为毕设、课程设计或工程实训参考。项目基于Python实现,核心包含云工作流调度建模、强化学习训练与评估流程,具备从算法代码到实验结果输出的完整链路。压缩包共135个文件,约11.21MB,主要类型包括py源码、pth模型权重、npy数据文件、pkl/JSON配置结果、Excel实验表格、PNG可视化图及TensorBoard事件文件,目录清晰,便于对照复现和二次开发。其中TensorBoard事件文件可直观查看训练收敛情况,模型权重与实验数据可用于继续训练或结果分析。目前已72人学习下载,适合需要快速理解深度强化学习在调度问题中落地方式的研究者参考借鉴。
1. 用深度强化学习调度云工作流,究竟比启发式算法强在哪
在调度系统里泡过几年的人,看到北京化工大学本科毕业设计《基于深度强化学习的云工作流调度》这个标题,大概会先愣一下:工作流调度不是被 HEFT 这类启发式算法统治了二十年吗,凑什么深度强化学习的热闹?但把问题摆出来就清楚了——云环境里任务一多,启发式算法抱着有向无环图算一棵静态调度树,遇到负载波动、异构实例、按秒计费的资源池,贪心策略经常把自己锁进局部最优里。深度强化学习算法把调度当成序列决策问题,用奖励信号逼着智能体自己摸出“什么时候把哪个任务发给哪台机器”的策略,训练好后一次推理就能出调度方案,不用在线重算。这篇毕设正好覆盖了从问题建模、模拟器改造到策略梯度训练的全链路,适合想把深度强化学习从教程代码迁移到真实优化问题的人,也适合只想要一套能复现的实验框架去应付课程设计、或者做自己云资源编排预研的从业者。说白了,这是一份能照着走通的代码包,不是纸上谈兵。
2. 云工作流调度问题建模:状态空间、动作空间与奖励函数怎么设计
本科毕设里最容易翻车的,不是神经网络搭不起来,而是把一个工程调度问题硬套进强化学习的框子里,结果环境写得像玩具,训练出来的策略换个 DAG 就不认账。这一章先解决最前置的问题:要调度的东西到底长什么样,以及怎么把它翻译成马尔可夫决策过程。
2.1 工作流 DAG 与异构资源模型:scheduler 到底在决策什么
云工作流调度的输入是一个有向无环图(DAG),节点是计算任务,边是数据或控制依赖。每个任务有四个基本属性:计算量(百万指令数 MI)、输入文件大小、输出文件大小、以及依赖它的父任务集合。父任务只有全部执行完,子任务才能开始,这就是工作流和普通批处理作业最本质的区别——调度器不能只看当前队列里的单个任务,还要照顾依赖链上的就绪关系。
资源侧是异构的虚拟机池。同一时刻可能有几十台不同规格的机器,单核算力不同、带宽不同、按秒的计费价格也不同。调度器要做两件事:一是把每个任务指派到某一台虚拟机上;二是决定所有任务在每台虚拟机上的执行顺序。前者是空间维度的指派,后者是时间维度的排序,合起来就是一组决策矩阵。一个完整调度方案的评估指标通常是总完工时间(makespan),或者 makespan 和租用成本的加权和。
常见的第一版状态参数表长这样:
| 对象 | 关键参数 | 调度器关心什么 |
|---|---|---|
| 任务节点 | 计算量、输入/输出大小、父任务集合 | 前驱是否就绪、传数据到某台 VM 要多久 |
| 依赖边 | 转移数据量 | 两个任务是否该放在同一台 VM 上,省传输时间 |
| 虚拟机 | MIPS、带宽、按秒费用、启动时延 | 跑得快但贵,还是跑得慢但便宜 |
| 全局约束 | 截止时间、预算上限 | 是否需要在 deadline 前强制高配实例 |
这里有一个新手常忽略的点:数据依赖的传输时间不是常数,它取决于“前驱任务在哪个 VM 上执行”。同一个任务,选不同 VM,不仅执行时间不同,还把一大堆下游任务的通信开销给改变了。所以调度的核心矛盾不是“哪台机器快”,而是“整个 DAG 的关键路径被谁拖住”。初版环境如果只按任务计算量贪心分配,训练出来的策略大概率不如 HEFT。
2.2 状态、动作、奖励的 MDP 映射:做特征工程而不是堆神经网络
把 DAG 和资源池转成强化学习接口,第一步是定义状态编码。常见做法是把三块特征拼成一个固定长度向量:任务层特征、虚拟机层特征、全局进度特征。固定长度很关键,因为策略网络是全连接层,输入维度不能随着任务和 VM 数量变化。所以要么设定一个最大规模并用掩码处理,要么把超出部分裁剪。任务特征至少包含归一化计算量、前驱是否全部完成、是否已被调度;虚拟机特征包含归一化算力、下一次空闲时间、单位成本。全局特征则放已完成任务比例和当前时间进度。
def encode_state(dag_graph, vm_list, schedule, now): """把 DAG + 资源池 + 当前调度进度编码成固定长度状态向量""" task_feat = [] for task in dag_graph.tasks: # 每个任务贡献 3 个特征:归一化计算量、前驱就绪标志、是否已调度 task_feat.extend([ task.mi / ref_mi, # 计算量除以参考值,压到量纲无关 float(task.preds_all_done), # 1 表示父任务全部完成,可用于动作掩码 float(task not in schedule), # 还没被调度的任务标记为 1 ]) vm_feat = [] for vm in vm_list: # 每台 VM 贡献 3 个特征:算力、下次空闲剩余时间、单位成本 vm_feat.extend([ vm.mips / max_mips, max(0.0, vm.next_free_time - now) / horizon, vm.cost_per_sec / ref_cost, ]) global_feat = [ len(schedule) / dag_graph.num_tasks, # 完成进度,鼓励先解决大头任务 (now - t_start) / horizon # 时间进度,防止初期乱撞 ] return torch.tensor(task_feat + vm_feat + global_feat, dtype=torch.float32)这段代码的关键在“归一化”和“掩码友好”两个设计。归一化不是随便除个最大值,这里 ref_mi、horizon 都要从训练集统计出来固定住,不能每次随 DAG 变化,否则同一策略在不同规模问题上特征分布漂移,迁移性会很差。preds_all_done 标志位专门给动作掩码用的:如果任务前驱没完成,这一位是 0,策略网络即便想选它,掩码也会把它过滤掉。前驱全完成的任务才进入候选动作集,这一条能砍掉大部分无效探索空间,训练收敛速度差好几倍。
动作空间有两种设计流派。第一种是“一次选一个任务-虚拟机对”,动作是二维离散组合,动作数量等于候选任务数乘虚拟机数;第二种是“先排序再指派”,策略网络输出一个任务顺序,虚拟机指派用贪心规则跟随后续填充。毕设里用二维离散组合比较直白,奖励按单步或按回合结算都有现成框子。我倾向把奖励设计成回合级稀疏奖励加一个小塑形项:回合结束返回负的 makespan,每一步给一个小的完工时间倒推差作为中间激励。纯稀疏奖励在任务数超过二十个时基本训不出来。
2.3 选定 PPO 而不是 DQN/DDPG:深度强化学习算法的稳定性与样本效率权衡
深度强化学习算法里,做离散动作调度首选近端策略优化,不用犹豫。DQN 在这种长 horizon 组合优化问题上要维护目标网络和回放缓冲区,而且只能做离策略学习,动作一多就陷入过估计;DDPG 是做连续控制的,把离散指派强行连续化成连续分配矩阵再取 argmax,细节多,奖励尺度一变就得重调。PPO 属于策略梯度家族,天然适配离散动作,通过裁剪代理目标函数限制每次更新幅度,是最不容易“跑着跑着奖励突然崩掉”的算法之一。
| 算法 | 动作类型 | 训练稳定性 | 样本效率 | 调度场景的主要坑 |
|---|---|---|---|---|
| DQN | 离散 | 中 | 高 | 大离散空间过估计,依赖经验回放调参 |
| DDPG / TD3 | 连续 | 中低 | 中 | 指派问题硬改连续,奖励尺度敏感 |
| A2C / A3C | 离散/连续 | 低 | 低 | 步长敏感,异步坑多 |
| PPO | 离散/连续 | 高 | 中 | 更新步数要控制,但总体最省心 |
PPO 另一个好处是实现了 clip 目标,更新策略时新旧策略的比率被限制在 1±clip 区间里,相当于给每次梯度上升套了一个保险绳。毕设环境往往缺乏真实任务,训练曲线不稳定就会浪费大量时间在“到底是算法问题还是环境问题”的排查上。用一个稳定性靠前的算法,可以把注意力集中在调度建模和奖励上,这是最实际的选型逻辑。
3. 复现路径:从模拟器到 PPO 训练的最小可跑通方案
建模做完,接下来是把环境跑起来。这一章讲一条能在一个周末内跑通的最小路径,包括模拟器选型、Gym 环境包装、训练脚本三个部分,最后给出基线和评估指标。
3.1 模拟器选型与 Gym 环境包装:WorkflowSim 的离散事件引擎不能直接吃
云工作流调度论文里最常见的基础设施是 WorkflowSim,它是基于 CloudSim 扩展出来的工作流模拟器,自带 DAG 生成器和一些调度算法。但 WorkflowSim 的事件驱动循环是整体跑完整个工作流然后输出日志,它没有“决策一次、环境前进一步”的接口,而强化学习恰恰需要这种增量交互。直接拿原始 WorkflowSim 喂给 PPO 是行不通的,必须包一层 Gym 环境。
常见的改造路线是:用 WorkflowSim 或者它的数据文件生成 DAG 和虚拟机配置,然后在自己的轻量离散事件模拟器里推进执行。我一般不会改动 WorkflowSim 内部调度源码,而是把它当成一个数据生成器,训练时期望得到 reward 快速反馈,模拟器越轻越好。轻量模拟器只需要维护一个全局时钟、每台虚拟机的空闲时间列表、以及尚未就绪的任务列表。任务被指派到 VM 后,计算它的最早可开始时间、执行时长、数据传输时间,然后推进时钟。整套逻辑代码量不大,但要注意事件排序的稳定性,后面避坑章会专门讲。
环境接口只要两个方法就够了。reset 负责初始化一个新的随机 DAG 和资源池,返回初始状态;step 接收一个动作,返回下一状态、奖励和结束标志。结束标志在任务全部完成时置真。一个关键惯例是:轮次结束返回的奖励用负 makespan,并除以参考基线(比如 HEFT 的 makespan)做归一化,这样不同规模的 DAG 才能共享相近的奖励尺度。
3.2 PPO 训练脚本:动作掩码、reward 归一化、GAE 这三件事必须做全
训练脚本表面是套一个现成 PPO 实现,但直接套容易踩三个隐形坑。第一,动作掩码缺失会导致智能体反复尝试不可行动作,浪费整个前几千步;第二,reward 不归一化会导致两个不同规模的工作流在同一批次里梯度互相拉扯;第三,不用 GAE 而用简单 TD 误差,优势估计方差过大,训练曲线像心电图。这三件事在一开始就写进代码,后面省事得多。
for epoch in range(total_epochs): obs_buffer, act_buffer, logp_buffer, adv_buffer = [], [], [], [] obs = env.reset() for step in range(rollout_len): # 策略网络返回动作分布,再套上动作掩码过滤非法任务-虚拟机组合 action_dist = agent.policy(obs) action_dist = MaskedCategorical(action_dist, mask=legal_actions(obs)) action = action_dist.sample() next_obs, reward, done = env.step(action) obs_buffer.append(obs); act_buffer.append(action) logp_buffer.append(action_dist.log_prob(action)) adv_buffer.append(reward) # 占位,下面统一算 GAE obs = next_obs if done: obs = env.reset() # 用 GAE 估计优势:lam 控制偏差方差折中,0.95 起步 advantages = compute_gae(adv_buffer, values, gamma=0.99, lam=0.95) for _ in range(update_epochs): # PPO clip 目标:ratio 太大时按 clip 区间截断,防止策略一步跑飞 ratio = (new_logp - old_logp).exp() loss = -torch.min(ratio * advantages, ratio.clamp(1.0 - clip, 1.0 + clip) * advantages).mean() optimizer.zero_grad(); loss.backward(); optimizer.step()学习率建议从 3e-4 起步,调度网络一般两层 256 宽就够,隐藏层超过 512 训练收益很小反而容易过拟合到训练 DAG 的结构上。update_epochs 不要超过 5,clip 系数 0.2 是默认值,奖励尺度差异大的时候可以调到 0.1。GAE 的 lam 参数在这个场景里有讲究:任务间依赖长,往前走太多步优势估计会引入大量噪声,0.95 经验值可以,若训练曲线波动剧烈就降到 0.9 试试。
3.3 基线与评估:HEFT、PSO、随机策略对照,以及 SLR 指标怎么读
毕设如果没有基线对比,说服力直接腰斩。工作流调度领域铁打的基线是 HEFT(异构最早完成时间优先),它是一类静态列表调度算法:先按 rank 值排任务优先级,再依次把任务塞进能最早完成的虚拟机。HEFT 是贪心里最稳的,也是评估时最该打的对手。另一个常用基线是 PSO(粒子群),它在线计算时间较长,适合作为“更贵的参考解”存在;实际应用里大家更在意 PPO 能不能在和 HEFT 差不多的时间内给出更好的总时间。
评估指标建议看三个:makespan、总成本、调度长度比 SLR。SLR 的定义是当前方案 makespan 除以 DAG 关键路径下界,即不考虑资源竞争、只算关键路径上的总执行时间。SLR 为 1 是理论上限,2 表示比理想状态慢一倍。这个指标天然消除了 DAG 规模不同导致的数值差异,最适合做跨工作流对比。成本指标则看租用虚拟机总时长乘以单价,因为 PPO 可能会为了压时间把所有任务堆到最贵实例上,makespan 好看但成本爆炸,所以奖励里要有成本加权项。
合理的实验设计是生成三组工作流:25 任务小规模、50 任务中规模、100 任务大规模,每种规模下随机生成多个 DAG 实例。训练时用小规模,评估时直接在三个规模上都测试,记录 SLR 和成本,和 HEFT、PSO 的结果放一张表。常见的可复现结果是:小规模上 PPO 能和 HEFT 打平或略好,中规模上 SLR 低 5% 到 10%,但训练时间需要几小时到一夜;如果在中等规模上打不过 HEFT,问题大概率出在奖励设计而不是算法上。
4. 关键参数与实验结果:哪些是算法玄学,哪些是能复现的硬参数
深度强化学习算法训练被人说玄学,多半是参数设置没有追踪记录。这一章把最影响结果的参数取舍讲透,再讲训练曲线的判读方法,最后说跨规模泛化怎么做验证。
4.1 六组必调参数:学习率、GAE lambda、clip、熵系数、reward 缩放、网络宽度
调度任务不是图像、不是 NLP,输入特征是手工统计量,维度也就几十,所以网络结构不是瓶颈。真正让训练翻车的是下面这六个参数:
| 参数 | 建议范围 | 对训练的影响 | 我的调试习惯 |
|---|---|---|---|
| 学习率 | 1e-4 ~ 5e-4 | 大了奖励发散,小了长时间平台期 | 从 3e-4 起,不收敛就降一半重跑 |
| GAE lambda | 0.90 ~ 0.98 | 控制优势估计偏差与方差 | 波动大调 0.9,效果上不去调 0.97 |
| clip 系数 | 0.1 ~ 0.3 | 更新步长上限 | 奖励出现尖峰调 0.1 |
| 熵系数 | 0.001 ~ 0.01 | 鼓励探索的量 | 前期 0.01,后期降到 0.001 |
| reward 缩放 | 使奖励范围落在 0.1 ~ 10 | 影响策略梯度步长感知 | 固定不随训练改,除非不收敛 |
| 隐藏层宽度 | 128 ~ 512 | 过宽过拟合 DAG 结构 | 256 为主,换数据集才改 |
reward 缩放最容易踩坑的是“初始奖励特别大导致策略一步僵死”。如果回合奖励是负 makespan,绝对值能到几万,直接进 PPO 的话优势信号差出好几个数量级,策略分布瞬间坍缩成只做一个动作。解决办法就是除以参考基线再压缩到 [-10, 0] 左右。参考基线用 HEFT 跑出来的 makespan,每个 DAG 实例算一次,缓存住即可。
熵系数也值得一提。它在损失函数里给策略熵一个加权重,防止策略太快收敛到单一动作。调度问题动作空间大、奖励稀疏,没有熵系数几乎必然过早收敛到随机策略。常见的经验是从 0.01 开始,看训练中后期熵值掉到接近 0 再降一半系数,相当于让智能体从“乱试”过渡到“专注”。
4.2 训练曲线判读:奖励掉头、SLR 平台期、cost 膨胀的三种翻车信号
我判断一个调度智能体有没有训好,从来不看最后一张测试表,而是看训练过程中的三条跟踪曲线:平均回合奖励、每轮动作熵、评估集 SLR。
第一种翻车信号是奖励中期突然掉头向下。前期奖励缓慢上升是正常,但如果尖锐的掉头出现在第几千步之后,最常见原因是学习率过高导致策略更新震荡,偶尔的坏批量把策略推到劣化区域。处理手段是回滚到掉头前的权重、把学习率减半再继续。
第二种是奖励在涨,但评估集 SLR 长期停在 1.8 以上不动。这是过拟合到训练 DAG 的典型症状,策略记住了具体任务组合而不是学出调度规律。解决方法是训练时每轮更换随机 DAG,同时把 DAG 的生成分布做得宽一些。还有一种可能是环境中某些任务长期得不到动作掩码更新,策略形成了固定偏好,这时候检查状态编码里就绪标志是否真的随着调度推进在变化。
第三种是 makespan 压下来了,成本却涨了。这通常是奖励函数里成本项的权重太小。需要把成本项从一个固定负数改成违反预算约束的惩罚,或者将目标从 min(makespan) 改成 min(makespan + lambda * cost),lambda 设为 0.1 起步。
4.3 跨规模泛化验证:训练 20 个任务,评估 50 个任务
很多毕设方案在训练数据上跑得漂亮,一换 DAG 规模就露馅。泛化验证的正确做法是训练和评估彻底隔离。训练只用 20 任务左右的浅 DAG,评估集准备三个档次:15 个任务的小规模、30 个任务的中规模、50 个任务的大规模。所有测试实例在训练前就固定 seed 生成好存成文件,评估时加载,不重新随机生成。这样测出来的泛化性才是可信的。
state 编码里前几步提到的“以参考值归一化”,就是为了让不同规模问题进入同一个网络时特征分布稳定。我遇到过训练 20 任务上表现很好,但 50 任务上 SLR 劣化的案例,最后定位到是全局进度特征里 len(schedule)/num_tasks 用了测试集的总任务数做分母,而这个值训练时从没见过,就完蛋了。固定一个最大任务规模做分母,而不是用当前实例的任务数,可以解决这一项漂移。
5. 避坑指南:训练不收敛与模拟器黑匣子的五个常见问题排查
这个毕设方案里,真正的硬骨头在环境而不是网络。以下五条都是顺着这条路踩过、也帮别人排查过的真实问题,每条按现象、原因、解决三段写。
5.1 奖励一直在负值震荡,训练几千步毫无上升迹象
现象是每轮结束的回合奖励都停在 -50 到 -60 之间,动作熵也快速掉到接近 0,策略像是“躺平”了。原因基本可以锁定在探索空间过大:动作空间是所有候选任务和所有虚拟机的笛卡尔积,早期策略分不清哪些动作合法,大部分试错都打到非法动作上,合法动作被浪费掉了。解决方法是上一章提过的动作掩码,把所有前驱未完成的任务、以及没有任何任务可分配的虚拟机直接从动作分布中剔除,然后强制要求至少有一个合法动作可选。掩码生效后,训练几千步内应该能看到奖励稳步抬升,如果没有,再把奖励从纯密集改成“每完成一个任务给一个小的正奖励”,相当于给稀疏信号加一块指路的牌子。
5.2 reward 逐步上涨,但测试集 makespan 反而比 HEFT 差
现象是训练曲线好看,评估却很惨。原因多半是奖励设计与真实目标背离:回合奖励里总成本项的权重太小,智能体把所有任务都塞到最快最贵的虚拟机上,单看奖励确实在涨,但评估以 makespan 和成本加权计分就露馅了。解决方法是重跑一遍基线实验,分别记录纯 makespan、纯成本、加权三项数据,看看智能体是牺牲了哪一项换来的奖励上涨。如果是成本爆炸,提高奖励里成本惩罚的系数;如果是 makespan 变大,检查回合奖励是不是用了“每一步减去当前已用时间”这种塑形项,导致智能体学会拖慢执行来减少每一步的惩罚。
5.3 同样的代码和超参,换一台机器效果差很多
现象是训练复现结果波动大,同一套参数在两台机器上跑出的测试 SLR 差 15% 以上。原因大多是 PyTorch 的算子在不同架构、不同库版本下的浮点累计顺序,以及随机数种子没有固定到环境生成上。解决方法是:第一,torch.manual_seed、numpy.random.seed、python random.seed 三处全固定,并且在同一进程里设置,不能分开写在不同函数里;第二,把 DAG 生成器单独放一个模块,它的随机源和训练循环隔离;第三,所有基线数值记录时附带 CPU 型号和库版本。这样排查起来才不会变成玄学。
5.4 模拟器时间回退或任务执行顺序错乱
现象是日志里某台虚拟机的完成时间比开始时间还早,或者一个父任务尚未完成,子任务却开始了。凡是出现这种问题,第一反应查事件队列的排序稳定性。轻量模拟器里最常见的坑是用浮点时间戳排序,两个任务预计完成时间相同时,Python 排序不稳定就可能导致状态更新错乱。解决方法是排序键用“时间戳 + 任务 id”的双字段组合,时间戳相同按任务 id 排,保证确定性。再就是每台虚拟机的下一次空闲时刻,必须在任务分配时基于该 VM 当前的空闲时刻计算,而不是基于全局时钟,多台 VM 并行推进时才会准确。
5.5 PPO 更新时显存溢出或者训练批次里出现 NaN
现象是训练到中途 loss 变成 NaN,或者 roll out 收集数据时显存爆炸。NaN 的常见来源是 log_prob 计算出 0 的对数值,即策略概率被 mask 压到 0 后取对数;解决方法是给 log_prob 加一个 1e-8 的 epsilon,或者掩码处理时不要直接乘 0,而是用 -inf 填到非法位置。显存溢出则是 rollout_len 和 update_epochs 设置过大,调度任务不是 3D 游戏,rollout_len 设 512 到 1024 一般足够,update_epochs 保持 3 到 5。真碰到资源限制,先把轨迹存储从 tensor 换 numpy 数组,训练时再转张量,能省不少内存。
6. 进阶:用领域随机化与评估集把毕设权重变成可迁移的调度策略
跑通 PPO 训练只是第一步,让这份毕设方案在真实业务里能用,还得过最后一关:迁移性。我的习惯性做法是领域随机化,而且比论文里写得还要保守一点。训练时不固定任务数量,而是从一个区间里随机采样 DAG 规模,虚拟机的数量和配置也随机漂移,成本单位价格和网络带宽随机浮动。这样智能体见到的不是某一个固定的工作流,而是分布在工作流生成器上的连续变化。代价是训练时间变长:随机化程度越高,收敛越慢,但如果目标是给一套通用的“调度策略”,这块训练时间很值得花。
评估集建议做成固定的五个折。每一个折里固定一千个 DAG 实例,来自不同的生成分支(比如 Montage、Sipht、CyberShake 这类经典工作流模板的形状差异)。每次改完奖励函数或网络结构,直接在这五折上全部跑一遍,记录平均 SLR 和成本。这样你就有了自己的“标尺”,不再被某一次实验中随机生成的坏 DAG 干扰判断。我自己的教训是早期只拿一两个实例反复测试,改了几版奖励后才知道那一两个实例恰好是策略的“主场”,一换到别的结构就露馅。
最后一个技巧是权重复用。你不需要每次都从随机初始化开始训练。把在 50 任务规模上训练好的策略权重存下来,换到 100 任务规模再训时,只把输入层和输出层的维度按新规模扩展,其他层直接载入旧权重,学习率降到 1e-4 继续微调,收敛速度通常能快一半以上。这套“先用小规模摸规律,再扩大规模微调”的路线,是深度强化学习算法在调度这类组合优化问题里最实在的降本手段。希望这些经验能帮你少走几个弯路,把这份毕设做成一个真正能落地的调度引擎。
本文还有配套的精品资源,点击获取