强化学习算法如何将GPU利用率提高到100%——在线强化学习如何将GPU利用率提升至100%
一直有个疑问,那就是“强化学习算法如何将GPU利用率提高到100%”,在一些论坛中也有人会提出这样的问题,但是一直也没有人比较正面的回答过这个问题,为此正好自己又想到了这么一个问题,于是想在这里正面的谈论下这个问题。
特别说明下,本文主要讨论的是在线强化学习算法(其实离线强化学习算法的这个问题可以参考下面的相关回答)
首先说下,强化学习算法由于计算过程包括多个部分,因此要想使强化学习算法的GPU利用率为100%就必然要使用并行化算法,串行方式下的强化学习算法是无法保证GPU利用率为100%的。
在线强化学习算法的并行化可以分为同步并行和异步并行,在同步并行化的情况下GPU的利用率也难以达到100%,因为GPU的利用率并不意味着GPU的核心的利用率,而是代表GPU的使用时长,而GPU在没有开启单独用户独享的模式下每个进程在调用GPU的时候都是独占使用的,因此同步并行化情况下GPU的利用率可以写为b/(a+b),其中b为每step计算过程中GPU的使用时长,a为每step计算过程中CPU的使用时长,因此如果b/(a+b)近似的等于100%,那么必然存在两种可能,第一种就是a足够小,另一种就是b足够大;之所以前面说到强化学习算法如果要想GPU的利用率为100%就必须使用并行化,其原因就是只有并行化才会减少a的时长,而b的时长却是无法无限制的增大的,因为强化学习所使用的网络往往比较简单,因此一次正反传的时间往往很短,唯一可以增加b的时长的方法就是增大batch_size,而由于GPU的计算特性所限制,在GPU的计算能力下batch_size为1和某个较大数值(如:32或64)下的单次计算时长相同,而大于这个数值的batch_size则会显著的增加GPU的计算时长(这里可以看做是使数值b增加),但是该种情况下并不会对计算性能有显著提升,比如batch_size为64时GPU的正反传是0.01秒,但是batch_size为128时则为0.02秒,该种方式支行减低整体计算的效率与提高GPU利用率的初衷相反(该种方式虽然会提高GPU利用率但是会使整体算法的计算效率下降),因此我们知道同步并行强化学习算法可以去提高GPU的利用率但是却无法到达100%;单step的CPU计算时长a是无法降低到足够小,而GPU的计算时长b也是无法增加到足够大,因此b/(a+b)是无法近似等于100%的。
有了上面的分析我们也就知道了这么一个结论,那就是只有异步的并行强化学习算法才可以保证GPU的利用率达到100%,当然这里讨论的都是在线强化学习算法,正是因为在线强化学习必须要在每step的计算中都要生成数据才导致出这个GPU利用率低的问题;虽然异步强化学习算法可以保证GPU的利用率为100%,但是其实这里有两种情况,分别为同策略的异步强化学习算法和异策略的异步强化学习算法。异策略的强化学习算法,如DQN算法的异步形式是可以保证GPU的利用率为100%的,同时该种异步方式是不会对算法性能起到不良影响的;但是同策略的强化学习算法就存在影响算法性能的问题,因为异步计算的方式难以保证同策略中的同策略要求,这样必然导致由于数据的生成策略和训练策略存在一定的分布偏差从而影响算法的整体性能,该种情况完全可以在GPU利用率为100%的情况下使算法的整体性能下降,这也是为什么A3C算法没有A2C算法好的一个主要原因。
可以说在不影响算法整体性能的情况下,通过并行方式加速计算且保证GPU利用率为100%的算法只有异步并行的异策略强化学习算法,如异步并行化的DQN,给出示意图:

上面讨论的都是在线学习强化学习算法,可以说由于强化学习算法的特殊性所以难以是GPU利用率达到100%,而将GPU利用率提高也难免会影响算法的整体性能,可以说往往没有使GPU利用率达到100%的强化学习算法才是计算效率最高的,如果一味的去求GPU的高利用率甚至会使强化学习算法更加难以收敛,如同策略算法。而同步并行的方式使GPU利用率到达100%,即使不影响算法收敛也会较大程度增加算法的复杂度,从而引发其他的问题。
这里再多说一下,由于强化学习的神经网络比较简单,难以在不影响整体性能的情况下增加单次计算的GPU时长,但是我们可以通过设置用户独享GPU的计算模式从而开启多个GPU进程,以此来提高GPU的使用率,该种情况往往两个或三个GPU进程就可以使GPU的利用率达到100%,但是这里就不仅存在数据生成策略间的同异步并行的问题也有了训练进程的同异步并行的问题,当然这种方式也有较为简单的形式,那就是在标准串行DQN基础上增加训练进程的数量,该种情况也可以保证GPU利用率100%。
在不影响算法收敛,不影响算法整体性能的情况下提升GPU利用率才是有意义的。
也正是因为使用GPU训练难以最大限度的提高运行效率,因此也有人提出使用CPU训练的方式,该种情况则需要使用大量的CPU进行计算,如A2C,而该种情况则可以使用HPC平台进行计算,往往也可以得到非常好的性能,而且CPU的扩展性往往强于GPU,因此如果你有足够多的CPU,那么你完全可以通过足够数量的CPU计算来获得高于GPU计算性能的表现,但是这里需要知道这么一个问题,那就是1个3090显卡好找,但是有着2000个CPU的HPC平台不好找,因此该种提升计算性能的方式不太适合个人用户而是比较适合企业用户。
PS:
大部分的在线强化学习算法难以在不影响算法收敛和整体性能的情况下提高GPU利用率,目前可以知道的可以提高GPU利用率到100%的在线情况学习算法大致有异步DQN和开启用户独占模式的并行训练的强化学习算法,当然即使是开启用户显卡独占也难以使同策略强化学习算法的GPU利用率为100%,这其中也难免要用到将异策略采样的数据通过重要性采样转为近似同策略的数据从而使多个训练进程使用,当然这种方式往往也难以就一定可以保证GPU利用率为100%,该种情况可以考虑在impala算法基础撒花姑娘进行改进。
=======================================================
这里给出一个实践的例子:
https://github.com/PacktPublishing/Deep-Reinforcement-Learning-Hands-On-Second-Edition
中的Chapter06中的DQN算法为例:
我们伪造假数据来用DQN的神经网络进行计算:
import torch import torch.nn as nn import numpy as np class DQN(nn.Module): def __init__(self, input_shape, n_actions): super(DQN, self).__init__() self.conv = nn.Sequential( nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4), nn.ReLU(), nn.Conv2d(32, 64, kernel_size=4, stride=2), nn.ReLU(), nn.Conv2d(64, 64, kernel_size=3, stride=1), nn.ReLU() ) conv_out_size = self._get_conv_out(input_shape) self.fc = nn.Sequential( nn.Linear(conv_out_size, 512), nn.ReLU(), nn.Linear(512, n_actions) ) def _get_conv_out(self, shape): o = self.conv(torch.zeros(1, *shape)) return int(np.prod(o.size())) def forward(self, x): conv_out = self.conv(x).view(x.size()[0], -1) return self.fc(conv_out) if __name__=="__main__": import torch.nn as nn import torch.optim as optim batch_size=64 model=DQN((4, 84, 84), 6).to("cuda:0") optimizer = optim.Adam(model.parameters(), lr=0.0001) while True: images=torch.rand((batch_size, 4, 84, 84), device="cuda:0") labels=torch.rand((batch_size, ), device="cuda:0") _label=model(images).max(1)[0] # _label=model(images)[:, 0] # _label=model(images) loss=nn.MSELoss()(_label, labels) optimizer.zero_grad() loss.backward() optimizer.step()
GPU的利用率为100%。
-----------------------------------------
标准的DQN算法:
#!/usr/bin/env python3 from lib import wrappers from lib import dqn_model import argparse import time import numpy as np import collections import torch import torch.nn as nn import torch.optim as optim from tensorboardX import SummaryWriter DEFAULT_ENV_NAME = "PongNoFrameskip-v4" MEAN_REWARD_BOUND = 19 GAMMA = 0.99 BATCH_SIZE = 32 REPLAY_SIZE = 10000 LEARNING_RATE = 1e-4 SYNC_TARGET_FRAMES = 1000 REPLAY_START_SIZE = 10000 EPSILON_DECAY_LAST_FRAME = 150000 EPSILON_START = 1.0 EPSILON_FINAL = 0.01 Experience = collections.namedtuple( 'Experience', field_names=['state', 'action', 'reward', 'done', 'new_state']) class ExperienceBuffer: def __init__(self, capacity): self.buffer = collections.deque(maxlen=capacity) def __len__(self): return len(self.buffer) def append(self, experience): self.buffer.append(experience) def sample(self, batch_size): indices = np.random.choice(len(self.buffer), batch_size, replace=False) states, actions, rewards, dones, next_states = \ zip(*[self.buffer[idx] for idx in indices]) return np.array(states), np.array(actions), \ np.array(rewards, dtype=np.float32), \ np.array(dones, dtype=np.uint8), \ np.array(next_states) class Agent: def __init__(self, env, exp_buffer): self.env = env self.exp_buffer = exp_buffer self._reset() def _reset(self): self.state = env.reset() self.total_reward = 0.0 @torch.no_grad() def play_step(self, net, epsilon=0.0, device="cpu"): done_reward = None if np.random.random() < epsilon: action = env.action_space.sample() else: state_a = np.array([self.state], copy=False) state_v = torch.tensor(state_a).to(device) q_vals_v = net(state_v) _, act_v = torch.max(q_vals_v, dim=1) action = int(act_v.item()) # do step in the environment new_state, reward, is_done, _ = self.env.step(action) self.total_reward += reward exp = Experience(self.state, action, reward, is_done, new_state) self.exp_buffer.append(exp) self.state = new_state if is_done: done_reward = self.total_reward self._reset() return done_reward def calc_loss(batch, net, tgt_net, device="cpu"): states, actions, rewards, dones, next_states = batch states_v = torch.tensor(np.array( states, copy=False)).to(device) next_states_v = torch.tensor(np.array( next_states, copy=False)).to(device) actions_v = torch.tensor(actions).to(device) rewards_v = torch.tensor(rewards).to(device) done_mask = torch.BoolTensor(dones).to(device) state_action_values = net(states_v).gather( 1, actions_v.unsqueeze(-1)).squeeze(-1) with torch.no_grad(): next_state_values = tgt_net(next_states_v).max(1)[0] next_state_values[done_mask] = 0.0 next_state_values = next_state_values.detach() expected_state_action_values = next_state_values * GAMMA + \ rewards_v return nn.MSELoss()(state_action_values, expected_state_action_values) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--cuda", default=False, action="store_true", help="Enable cuda") parser.add_argument("--env", default=DEFAULT_ENV_NAME, help="Name of the environment, default=" + DEFAULT_ENV_NAME) args = parser.parse_args() device = torch.device("cuda" if args.cuda else "cpu") env = wrappers.make_env(args.env) net = dqn_model.DQN(env.observation_space.shape, env.action_space.n).to(device) tgt_net = dqn_model.DQN(env.observation_space.shape, env.action_space.n).to(device) writer = SummaryWriter(comment="-" + args.env) print(net) buffer = ExperienceBuffer(REPLAY_SIZE) agent = Agent(env, buffer) epsilon = EPSILON_START optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE) total_rewards = [] frame_idx = 0 ts_frame = 0 ts = time.time() best_m_reward = None while True: frame_idx += 1 epsilon = max(EPSILON_FINAL, EPSILON_START - frame_idx / EPSILON_DECAY_LAST_FRAME) reward = agent.play_step(net, epsilon, device=device) if reward is not None: total_rewards.append(reward) speed = (frame_idx - ts_frame) / (time.time() - ts) ts_frame = frame_idx ts = time.time() m_reward = np.mean(total_rewards[-100:]) print("%d: done %d games, reward %.3f, " "eps %.2f, speed %.2f f/s" % ( frame_idx, len(total_rewards), m_reward, epsilon, speed )) writer.add_scalar("epsilon", epsilon, frame_idx) writer.add_scalar("speed", speed, frame_idx) writer.add_scalar("reward_100", m_reward, frame_idx) writer.add_scalar("reward", reward, frame_idx) if best_m_reward is None or best_m_reward < m_reward: torch.save(net.state_dict(), args.env + "-best_%.0f.dat" % m_reward) if best_m_reward is not None: print("Best reward updated %.3f -> %.3f" % ( best_m_reward, m_reward)) best_m_reward = m_reward if m_reward > MEAN_REWARD_BOUND: print("Solved in %d frames!" % frame_idx) break if len(buffer) < REPLAY_START_SIZE: continue if frame_idx % SYNC_TARGET_FRAMES == 0: tgt_net.load_state_dict(net.state_dict()) optimizer.zero_grad() batch = buffer.sample(BATCH_SIZE) loss_t = calc_loss(batch, net, tgt_net, device=device) loss_t.backward() optimizer.step() writer.close()
GPU的利用率为 40% 。
-----------------------------------------
如果我们把训练过程中的数据生成部分屏蔽掉,代码:
#!/usr/bin/env python3 from lib import wrappers from lib import dqn_model import argparse import time import numpy as np import collections import torch import torch.nn as nn import torch.optim as optim from tensorboardX import SummaryWriter DEFAULT_ENV_NAME = "PongNoFrameskip-v4" MEAN_REWARD_BOUND = 19 GAMMA = 0.99 BATCH_SIZE = 32 #32 REPLAY_SIZE = 10000 LEARNING_RATE = 1e-4 SYNC_TARGET_FRAMES = 1000 REPLAY_START_SIZE = 10000 EPSILON_DECAY_LAST_FRAME = 150000 EPSILON_START = 1.0 EPSILON_FINAL = 0.01 Experience = collections.namedtuple( 'Experience', field_names=['state', 'action', 'reward', 'done', 'new_state']) class ExperienceBuffer: def __init__(self, capacity): self.buffer = collections.deque(maxlen=capacity) def __len__(self): return len(self.buffer) def append(self, experience): self.buffer.append(experience) def sample(self, batch_size): indices = np.random.choice(len(self.buffer), batch_size, replace=False) states, actions, rewards, dones, next_states = \ zip(*[self.buffer[idx] for idx in indices]) return np.array(states), np.array(actions), \ np.array(rewards, dtype=np.float32), \ np.array(dones, dtype=np.uint8), \ np.array(next_states) class Agent: def __init__(self, env, exp_buffer): self.env = env self.exp_buffer = exp_buffer self._reset() def _reset(self): self.state = env.reset() self.total_reward = 0.0 @torch.no_grad() def play_step(self, net, epsilon=0.0, device="cpu"): done_reward = None if np.random.random() < epsilon: action = env.action_space.sample() else: state_a = np.array([self.state], copy=False) state_v = torch.tensor(state_a).to(device) q_vals_v = net(state_v) _, act_v = torch.max(q_vals_v, dim=1) action = int(act_v.item()) # do step in the environment new_state, reward, is_done, _ = self.env.step(action) self.total_reward += reward exp = Experience(self.state, action, reward, is_done, new_state) self.exp_buffer.append(exp) self.state = new_state if is_done: done_reward = self.total_reward self._reset() return done_reward def calc_loss(batch, net, tgt_net, device="cpu"): states, actions, rewards, dones, next_states = batch states_v = torch.tensor(np.array( states, copy=False)).to(device) next_states_v = torch.tensor(np.array( next_states, copy=False)).to(device) actions_v = torch.tensor(actions).to(device) rewards_v = torch.tensor(rewards).to(device) done_mask = torch.BoolTensor(dones).to(device) state_action_values = net(states_v).gather( 1, actions_v.unsqueeze(-1)).squeeze(-1) with torch.no_grad(): next_state_values = tgt_net(next_states_v).max(1)[0] next_state_values[done_mask] = 0.0 next_state_values = next_state_values.detach() expected_state_action_values = next_state_values * GAMMA + \ rewards_v return nn.MSELoss()(state_action_values, expected_state_action_values) first = True states_v = None next_states_v = None actions_v = None rewards_v = None done_mask = None def calc_loss2(batch, net, tgt_net, device="cpu"): global first global states_v,next_states_v,actions_v,rewards_v,done_mask """ if first: states, actions, rewards, dones, next_states = batch states_v = torch.tensor(np.array( states, copy=False)).to(device) next_states_v = torch.tensor(np.array( next_states, copy=False)).to(device) actions_v = torch.tensor(actions).to(device) rewards_v = torch.tensor(rewards).to(device) done_mask = torch.BoolTensor(dones).to(device) first=False """ states, actions, rewards, dones, next_states = batch states_v = torch.tensor(np.array( states, copy=False)).to(device) next_states_v = torch.tensor(np.array( next_states, copy=False)).to(device) actions_v = torch.tensor(actions).to(device) rewards_v = torch.tensor(rewards).to(device) done_mask = torch.BoolTensor(dones).to(device) first=False state_action_values = net(states_v).gather( 1, actions_v.unsqueeze(-1)).squeeze(-1) with torch.no_grad(): next_state_values = tgt_net(next_states_v).max(1)[0] next_state_values[done_mask] = 0.0 next_state_values = next_state_values.detach() expected_state_action_values = next_state_values * GAMMA + \ rewards_v return nn.MSELoss()(state_action_values, expected_state_action_values) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--cuda", default=False, action="store_true", help="Enable cuda") parser.add_argument("--env", default=DEFAULT_ENV_NAME, help="Name of the environment, default=" + DEFAULT_ENV_NAME) args = parser.parse_args() device = torch.device("cuda" if args.cuda else "cpu") env = wrappers.make_env(args.env) net = dqn_model.DQN(env.observation_space.shape, env.action_space.n).to(device) tgt_net = dqn_model.DQN(env.observation_space.shape, env.action_space.n).to(device) writer = SummaryWriter(comment="-" + args.env) print(net) buffer = ExperienceBuffer(REPLAY_SIZE) agent = Agent(env, buffer) epsilon = EPSILON_START optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE) total_rewards = [] frame_idx = 0 ts_frame = 0 ts = time.time() best_m_reward = None while True: frame_idx += 1 if frame_idx < 20000: epsilon = max(EPSILON_FINAL, EPSILON_START - frame_idx / EPSILON_DECAY_LAST_FRAME) reward = agent.play_step(net, epsilon, device=device) if reward is not None: total_rewards.append(reward) speed = (frame_idx - ts_frame) / (time.time() - ts) ts_frame = frame_idx ts = time.time() m_reward = np.mean(total_rewards[-100:]) print("%d: done %d games, reward %.3f, " "eps %.2f, speed %.2f f/s" % ( frame_idx, len(total_rewards), m_reward, epsilon, speed )) writer.add_scalar("epsilon", epsilon, frame_idx) writer.add_scalar("speed", speed, frame_idx) writer.add_scalar("reward_100", m_reward, frame_idx) writer.add_scalar("reward", reward, frame_idx) if best_m_reward is None or best_m_reward < m_reward: torch.save(net.state_dict(), args.env + "-best_%.0f.dat" % m_reward) if best_m_reward is not None: print("Best reward updated %.3f -> %.3f" % ( best_m_reward, m_reward)) best_m_reward = m_reward if m_reward > MEAN_REWARD_BOUND: print("Solved in %d frames!" % frame_idx) break if len(buffer) < REPLAY_START_SIZE: continue if frame_idx % SYNC_TARGET_FRAMES == 0: tgt_net.load_state_dict(net.state_dict()) optimizer.zero_grad() batch = buffer.sample(BATCH_SIZE) loss_t = calc_loss(batch, net, tgt_net, device=device) loss_t.backward() optimizer.step() continue optimizer.zero_grad() loss_t = calc_loss2(batch, net, tgt_net, device=device) loss_t.backward() optimizer.step() writer.close()
GPU的利用率为 100% 。
其中有意思的一个地方是,发现下面代码:
def calc_loss(batch, net, tgt_net, device="cpu"): states, actions, rewards, dones, next_states = batch states_v = torch.tensor(np.array( states, copy=False)).to(device) next_states_v = torch.tensor(np.array( next_states, copy=False)).to(device) actions_v = torch.tensor(actions).to(device) rewards_v = torch.tensor(rewards).to(device) done_mask = torch.BoolTensor(dones).to(device)
CPU 的使用率为99%,也就是说上面的这个numpy转pytorch的操作会使CPU的利用率为接近100%。
从这里我们可以知道,如果要保证GPU的利用率为100%,那么就必须使调用GPU计算的进程只进行GPU的前后向计算,即使是这种类型转换的操作也应该由数据生成进程完成,这样才可以保证训练进程的GPU利用率为100% 。
PS:
在强化学习算法中,使GPU利用率为100%的最本质的原理就是在GPU计算的时候覆盖掉CPU端的计算,而这种情况则要使用多进程或多线程并行,同时我们也需要保证GPU端有足够的数据以保证GPU不空转;但是保证这样的条件往往会使算法的收敛性和整体性能受到影响,因此有时候过分追求GPU的高利用率是没有意义和价值的,即使有的类型的强化学习算法可以通过并行的方式使GPU利用率达到100%并且不影响算法性能,但是也会大幅度提高算法的复杂度,从而引起其他的难以预知的问题。
-----------------------------------------
posted on 2023-08-19 19:38 Angry_Panda 阅读(1472) 评论(0) 收藏 举报
浙公网安备 33010602011771号