第一次了解强化学习,这篇论文作为第一次尝试,相关代码会持续地同步更新到:Decima
1. 简介
Decima
: A general-purpose scheduling service for data processing jobs with dependent stages
Decima将所有调度策略编码在一个神经网络中, 通过大量的模拟实验来训练这个神经网络。这些实验中,每个实验对应一种工作负载的调度,观察输出,并且逐渐改进策略。
Decima的价值不仅仅在于如何运用强化学习(Reinforcement Learning, RL),旧瓶装新酒。为了成功地学习高效地调度策略,我们需要设计出一种高效的数据、调度行为的表现形式。
1. Decima面临的问题:
- 集群工作调度器必须要能处理成百上千的机器、任务以及每个任务数量庞大的配置项.问题规模庞大.
- 传统的RL算法无法用持续的流式任务训练模型,此外持续不断到来的job会让一些job不断堆积迟迟得不到“运行”
- 在训练前期提前中止训练,并且逐渐增加训练开始到中止训练的这个时间点,来让神经网络逐渐学会
短任务优先
- 随机输入(stochastic input)
- 在训练前期提前中止训练,并且逐渐增加训练开始到中止训练的这个时间点,来让神经网络逐渐学会
2. 有依赖的任务调度(Dependency-aware task scheduling):
每个Job DAG都有不同的阶段(stages), 每个阶段会有不同的任务执行间隔时间(duration)和并行的任务个数(parallel task)
理想的调度应该是, 所有独立的阶段(stages)应该尽可能地并行执行. 但是要处理这样地DAG调度是非常复杂的,目前很多调度方法只是简单地将各个阶段压入队列中或者是任意地执行各个阶段.
3. 设置合适的并行度(Setting the right level of parallellism)
一个理想的调度器必须能够清楚如何为各个job分配合适的资源. 一个公平分配资源的调度器可能未必会比不公平分配资源的调度器效率要更高, 一味地增大并行度有时效率并不会增大多少.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9oW6q6MD-1620735529882)(https://z3.ax1x.com/2021/05/08/gGq8N6.png)]
Decima 会寻找一个合适的并行度–Sweet Spot
, 该点之后的并行度与运行时间曲线的变化将会大幅度减少。
2. Spark的DAG调度问题
- 为一个job分配多少处理器
- 决定每一个job接下来哪个task开始运行
- 当一个处理器空闲时决定分配给哪个task
3. 设计思想与挑战
Decima 使用一个神经网络来完成决策,这个神经网络称为
policy network
Scheduleing Event:
stage completion
: 释放资源(frees up executors)job arrival
: DAG图增加节点(adds a DAG)...
Decima以这些事件作为输入, 输出一个调度动作(Scheduler Action). 该算法通过RL来训练它的神经网络,输入是离线模拟的实验(offline simulated experiments). 在这些实验中,Decima尝试去调度一个工作流,观察输出并给出该调度的一个评价指标。
Challenges:
弹性的状态信息处理(Scalable state information processing)
:
调度器必须考虑大量的动态信息来做决策: 数百个DAG图, 每个DAG图有上百个阶段, 同时处理器个数也可能各不相同.
解空间范围庞大(Huge space of scheduling decisions)
:
一个调度器要为成千上万个阶段(stages)去分配处理器, 这可能指数级的复杂度, 而RL算法必须在庞大的解空间中找到一个合适的策略
持续不断地数据源(Training for continous stochastic job arrivals)
:
随着时间推移,调度器会接收持续不断到来的job
4. 设计细节
1. 处理弹性的状态信息(Scalable state information processing)
:
-
方案一: 将所有状态信息编码为一个特征向量
- 缺点: 无法处理任意数量和大小的DAG图. 即使状态信息的数量固定,训练时也难以处理一个高维度的向量
-
方案二:
图神经网络(graph neural network)
- 思路: 将状态信息(如job的属性、DAG图的依赖关系)编码(encode)或者嵌入(embed)
图嵌入(graph embedding)
以DAG图为输入, 其节点带有阶段(stages)的属性(例如: 未执行的任务数量, 任务执行的时间间隔), 输出三类嵌入结果:
节点级别的嵌入结果(per-node embeddings)
: 捕捉节点和其子节点相关的信息。例如关键路径
上的任务集合job级别的嵌入结果(per-job embeddings)
: 整个DAG图的总体信息(例如job里的全部工作)全局嵌入(global embedding)
: 结合了所有job级别嵌入结果,组合为集群级别的总结(cluster-level summary), 例如job数量和集群负载
节点级别嵌入(per-node embeddings):
G i G_i Gi : DAG图
X v i X_v^i Xvi: DAG图中阶段(stage)的属性向量(每个阶段在DAG中代表一个节点)
( G i , X v i ) ⟼ e v i (G_i, X_v^i) \longmapsto e_v^i (Gi,Xvi)⟼evi: Dcima构建的per-node embedding
e v i e_v^i evi是一个向量, 代表了所有从节点 v v v可以到达的节点的信息.
Decima从DAG图的叶节点 v v v开始, 将所有子节点的信息传播到其父节点,以此类推, 得到如下式子
e v i = g [ ∑ u ∈ ξ ( v ) f ( e u i ) ] + X v i e_v^i = g[\sum_{u\in \xi(v) }f(e_u^i)] + X_v^i evi=g[u∈ξ(v)∑f(eui)]+Xvi
f ( ⋅ ) f(\cdot) f(⋅)和 g ( ⋅ ) g(\cdot) g(⋅)是向量输入的非线性变换, 通过小型的神经网络实现.
ξ ( v ) \xi(v) ξ(v) 是v的子节点
第一个式子是一个聚合操作, 第一项代表结合了v所有子节点的嵌入结果, 第二项代表v节点本身的特征向量. f ( ⋅ ) f(\cdot) f(⋅) 和 g ( ⋅ ) g(\cdot) g(⋅) 这两个非线性变换在所有DAG图节点中、所有消息传递(message passing,指的就是子节点的嵌入信息传递给父节点)中复用。
任务级别和全局嵌入(Per-job and global embeddings)
任务级别的嵌入中, 图神经网络会计算DAG图中所有节点嵌入信息的综合结果:
{
(
X
v
i
,
e
v
i
)
,
v
∈
G
i
}
⟼
y
i
\{(X_v^i, e_v^i), v\in G_i\} \longmapsto y^i
{(Xvi,evi),v∈Gi}⟼yi
全局嵌入中, 图神经网络会计算所有DAG图嵌入信息的综合结果:
{
y
1
,
y
2
,
.
.
.
}
⟼
z
\{y^1, y^2, ...\} \longmapsto z
{y1,y2,...}⟼z
Decima为每个DAG图增加一个节点, 这个节点是DAG中所有节点的父节点, 因此该节点的综合嵌入信息就是任务级别的嵌入结果.
Decima还为所有DAG图增加了一个全局总结节点
, 该节点囊括了所有DAG图的嵌入结果
每一个层级的嵌入都有其自身的非线性变换f和g, 因而Decima的图神经网络中共有6种非线性变换, 每种层级的嵌入各有两个.
2. 将调度决策进行编码(Encoding scheduling decisions as actions)
为了将处理器分配给任务的各个阶段, 一般有两种策略:
- 一次性的为所有任务阶段分配好处理器
- 这种策略存在
指数级别的解空间大小
, 排列组合的情况太多了
- 这种策略存在
- 每次当处理器空闲时就调用调度器来决定为哪个阶段(一个阶段就是DAG中一个节点)分配处理器:
- 这种策略每次的解空间较小, 但是会有
较长的动作序列
来对给定的任务集合进行调度
- 这种策略每次的解空间较小, 但是会有
Decima将解空间大小和动作序列的长度进行平衡, 通过将调度决策分解为一系列的
二维
的动作
这个二维动作
包含:
{
一个接下来将要被调度的阶段
该阶段执行所需要处理器个数的上界
\begin{cases} \text{一个接下来将要被调度的阶段} \\ \text{该阶段执行所需要处理器个数的上界} \end{cases}
{一个接下来将要被调度的阶段该阶段执行所需要处理器个数的上界
**可能的调度事件: **
Dcima进行调度的时机: DAG图状态发生变化时且有可运行的阶段时
- 一个阶段中所有的task执行完毕
- 一个阶段完成, 让其子节点可以开始准备执行
- 一个新的阶段到达系统中
在每一个调度事件中, 调度器会调度若干个空闲处理器。具体来说,调度器会将嵌入向量(embedding vectors)
输入到策略网络(policy network)
中, 得到一个二维的动作输出
:
⟨
v
,
l
i
⟩
\lang v,l_i\rang
⟨v,li⟩ . 该输出由阶段
v
v
v和
v
v
v的并行度约束(parallelism limit)
l
i
l_i
li 组成.
如果job目前拥有的excutor数目比需求上限要少, Decima为其分配足够处理器; 如果完成一次调度动作之后, 仍然有空闲的处理器, 调度器会输出另一个二维的动作, 再次进行分配. 直到没有处理器可用、或者没有可运行的阶段为止。
Decima为每个job设置的parallelism limit会略大于该job所需的处理器个数,这样每个job总会有至少一个处理器是空闲的。
选择Stage:
一个调度事件所处的时间 t t t
该时刻对应的状态(state)为 s t s_t st
对于job i i i中的节点 v v v, 策略网络计算一个score q v i = △ q ( e v i , y i , z ) q_v^i \overset{\triangle}{=}q(e^i_v, y^i, z) qvi=△q(evi,yi,z). q ( ⋅ ) q(\cdot) q(⋅) 是一个映射函数, 将嵌入向量(图神经网络的输出)映射为标量.
类似于嵌入步骤, 该函数也是一个由神经网络实现非线性的映射函数.
score
q
v
i
q_v^i
qvi代表调度节点v的优先级, Decima使用Softmax函数计算每个节点被选择的概率:
P
(
n
o
d
e
=
v
)
P(node=v)
P(node=v)
P
(
n
o
d
e
=
v
)
=
e
x
p
(
q
v
i
)
∑
u
∈
A
t
e
x
p
(
q
u
j
(
u
)
)
P(node=v) = \frac{exp(q_v^i)}{\sum_{u\in\ \mathcal{A}_t}exp(q_u^{j(u)})}
P(node=v)=∑u∈ Atexp(quj(u))exp(qvi)
A
t
\mathcal{A}_t
At是目前可被调度的节点集合,
j
(
u
)
j(u)
j(u)是节点
u
u
u所在的job
**并行度约束的选择(Parallesim limit selection): **
对于每一个job
i
i
i , Decima的策略网络会计算一个score:
w
l
i
=
△
w
(
y
i
,
z
,
l
)
w_l^i \overset{\triangle}{=}w(y^i,z,l)
wli=△w(yi,z,l)
该score用于给每个job
i
i
i分配一个并行度约束(parallelism limit)
l
l
l. 类似于选择stage的策略, Decima也使用一个softmax函数来计算选择哪一个并行度的概率.
Decima对所有的job和所有的并行度约束使用相同的函数 w ( ⋅ ) w(\cdot) w(⋅)
每一个调度事件发生时,Decima会挑选一个阶段(stage) v v v,同时为 v v v所处的job i i i 设置一个新的并行度.
Decima会调度处理器, 使得job i i i 拥有的处理器个数与其新的并行度约束相匹配。
通过使用不同的并行度重复调度动作(action),Decima能够为特定的stage提供合适的处理器数目。
3. 训练(Training):
训练Decima
RL训练过程包含多轮(episode), 每一轮包含若干调度事件(scheduleing event), 每个调度事件包含若干个动作(actions).
T T T 是一轮中总共的动作数量
t k t_k tk是第 k k k个动作的对应时钟数
Decima为每个动作计算一个reward r k = − ( t k − t k − 1 ) J k r_k=-(t_k-t_{k-1})J_k rk=−(tk−tk−1)Jk . J k J_k Jk代表时钟区间 [ t k − 1 , t k ) [t_{k-1}, t_k) [tk−1,tk) 内的任务数量.
RL算法的目的是最小化目标函数时间序列上的均值 :
average JCT
=
E
[
1
/
t
T
∑
k
=
1
T
(
t
k
−
t
k
−
1
)
J
k
]
\text{average JCT} = \mathbb{E}[1/t_T\sum_{k=1}^{T}(t_k-t_{k-1})J_k]
average JCT=E[1/tTk=1∑T(tk−tk−1)Jk]
该目标函数最小化了系统中的平均任务(job)数量
Decima的所有运算中: graph neural network 和 policy network都是可微分的。
将所有参数统一标志为: θ \theta θ
将所有的调度策略统一标志为: π θ ( s t , a t ) \pi_{\theta}(s_t,a_t) πθ(st,at), 定义为在状态(state) t t t 下选择动作 a t a_t at的概率.
考虑一个时钟刻度长度为 T T T 的轮次(episode),算法不断收集(state,action,reward)三个结果,例如在第k步,收集到 ( s k , a k , r k ) (s_k,a_k,r_k) (sk,ak,rk).
算法使用REINFORCE策略更新调度策略的
π
θ
(
s
t
,
a
t
)
\pi_\theta(s_t,a_t)
πθ(st,at)的参数
θ
\theta
θ:
θ
←
θ
+
α
∑
k
=
1
T
∇
θ
l
o
g
π
θ
(
s
k
,
a
k
)
(
∑
k
′
=
k
T
r
k
′
−
b
k
)
\theta \leftarrow \theta+\alpha\sum_{k=1}^{T}\nabla_\theta log\pi_\theta(s_k,a_k)\Big(\sum_{k'=k}^{T}r_{k'}-b_k \Big)
θ←θ+αk=1∑T∇θlogπθ(sk,ak)(k′=k∑Trk′−bk)
α
\alpha
α是learning rate,
b
k
b_k
bk是基准线(baseline). 基准线的一种算法就是将
b
k
b_k
bk设为reward的累计值(k步以前)的均值
很明显, ( ∑ k ′ r k ′ − b k ) (\sum_{k'r_{k'}-b_k}) (∑k′rk′−bk)衡量了总共的reward是多么好或者多么差。
∇ θ l o g π θ ( s k , a k ) \nabla_{\theta}log\pi_\theta(s_k,a_k) ∇θlogπθ(sk,ak)提供了参数空间中优化方向,以此来增加在状态 s k s_k sk中选择动作 a k a_k ak.
Challenge: Training with continous job arrivals:
- 训练初期,Decima的初始策略可能很差,不能在job到来时快速进行立即调度,从而导致了很多任务被堆积。一个良好的决策应该能尽可能减少等待时间,因此在训练初期如果有大量空闲时间(idle state),会尽早中止初始的几个迭代(episodes)。训练中会逐渐增加每次迭代中的步数,因此刚开始算法会主要学习调度短作业优先。随着其调度算法的改进,我们增加每次迭代的步数,使得问题更具挑战性。
这个逐渐增加任务序列长度(或者说问题复杂度)的方法实现了课程式学习(curriculum learning)--从易到难的思想.
Challenge: Variance caused by stochastic job arrivals:
每个训练轮次需要包含不同的任务到达模式
如果有种任务到达模式, 在某一时刻会突然到来大量的任务, 导致任务堆积, 那么该action的罚值会非常大, 而这并非由算法导致的, 而是随机的任务到达模式引起.
计算baseline b k b_k bk时, 只对具有同一任务达到模式的轮次的reward求均值, 而非对所有轮次求均值