更新: 重写多GPU程序示例, 2018-4-18
先感叹一波,我竟然会时隔一年后, 再更新这个, 本来是不想更新的,错了就错了, 根本没人看, 然而网友的评论说出现了问题, 我想还是更新一波吧, 不说了,真要给自己点个赞.
Torch的多GPU程序其实并不复杂.
最简单的代码:
require 'optim'
require 'nn'
require 'cutorch'
require 'cunn'
require 'torch'
torch.setdefaulttensortype('torch.FloatTensor')
local conv_net = nn.Sequential()
local concat = nn.ConcatTable()
concat:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
concat:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(concat)
local jt = nn.JoinTable(2)
conv_net:add(jt)
conv_net:add(nn.SpatialConvolution(6,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:cuda() -- 把网络的参数变成cuda类型
num_epochs = 1
batchsize = 64
criterion = nn.MSECriterion():cuda()
input = torch.randn(1000,3,256,256):mul(100):floor():cuda()
target = torch.randn(1000,3,256,256):mul(100):floor():cuda()
num_takes = input:size(1)/batchsize
-- DataParrallelTable是进行
-- 使用DataParallelTable进行多GPU, 1表示在batch维度进行切分
dpt = nn.DataParallelTable(1)
-- 使用第1,2,3,4个GPU, 将相同一个网络, 分别复制到4个GPU上.
for i = 1, 4 do
cutorch.setDevice(i)
dpt:add(conv_net:clone():cuda(), i)
end
print(net)
cutorch.setDevice(1) -- 设置`主`GPU, 同步时, 就是用来将其他GPU参数全部累加到这个GPU上.
parameters, gradParameters = dpt:getParameters()
-- TRAINING:
local timer = torch.Timer()
for i = 1, num_epochs do
for bth = 1, num_takes do
local input_bt = input:narrow(1, (bth-1)*batchsize+1, batchsize)
local target_bt = target:narrow(1, (bth-1)*batchsize+1, batchsize)
print('Processing epoch: ',i,' , batch: ',bth)
-- 如果你想要每个batch所需要的时间, 那么
-- 需要在 这里加上
-- local timer = torch.Timer()
-- dpt:syncParameters()
feval = function(x)
dpt:zeroGradParameters()
local output = dpt:forward(input_bt)
local err = criterion:forward(output, target_bt)
local gradOutput = criterion:backward(output, target_bt)
local gradInput = dpt:backward(input_bt, gradOutput)
return err, gradParameters
end
optim.sgd(feval, parameters, optimState)
dpt:syncParameters() -- 必须加入这个, 同步网络参数,才能进行下一步更新.
-- 如果你想要每个batch所需要的时间, 那么
-- 需要在 这里加上
-- print('Elapse time: '..timer:time().real..' seconds')
end
end
print('Totally elapse time: '..timer:time().real..' seconds')
因为这个方法是比较老的方法, 所以思想还是比较Naive:
因为采用的是同步模式训练方法, 所以 是先将一个网络在主GPU上构建出来, 然后复制到其他的GPU上, 然后更新. 每个GPU单独对拿到的一个batch的一小部分(batchsize/num_gpus)进行前向, 再反向. 得到的梯度要进行算平均, 即batch的平均梯度. 然后更新主
GPU的梯度(optim.sgd是对主
GPU进行操作), 再 net:syncParameters()一下, 这个是用来同步GPU的梯度, 就是将主
GPU的新的参数, 赋值给所有的GPU上, 使得所有GPU上的参数保持一致(这也就是为什么如果要计算一个batch所花的时间, 要进行syncParameters()一下,). 然后再用这个平均梯度, 更新每个GPU上的参数.
输出是
DataParallelTable: 4 x nn.Sequential {
[input -> (1) -> (2) -> (3) -> (4) -> (5) -> (6) -> (7) -> (8) -> (9) -> (10) -> output]
(1): nn.ConcatTable {
input
|`-> (1): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
`-> (2): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
... -> output
}
(2): nn.JoinTable
(3): nn.SpatialConvolution(6 -> 3, 3x3, 1,1, 1,1) without bias
(4): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(5): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(6): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(7): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(8): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(9): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(10): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
}
Processing epoch: 1 , batch: 1
Processing epoch: 1 , batch: 2
Processing epoch: 1 , batch: 3
Processing epoch: 1 , batch: 4
Processing epoch: 1 , batch: 5
Processing epoch: 1 , batch: 6
Processing epoch: 1 , batch: 7
Processing epoch: 1 , batch: 8
Processing epoch: 1 , batch: 9
Processing epoch: 1 , batch: 10
Processing epoch: 1 , batch: 11
Processing epoch: 1 , batch: 12
Processing epoch: 1 , batch: 13
Processing epoch: 1 , batch: 14
Processing epoch: 1 , batch: 15
Totally elapse time: 15.530827045441 seconds
可以看到 DataParallelTable: 4 x
表示有4块, 网络是什么什么. 我们再试试 2个GPU
2个GPU的输出
DataParallelTable: 2 x nn.Sequential {
[input -> (1) -> (2) -> (3) -> (4) -> (5) -> (6) -> (7) -> (8) -> (9) -> (10) -> output]
(1): nn.ConcatTable {
input
|`-> (1): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
`-> (2): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
... -> output
}
(2): nn.JoinTable
(3): nn.SpatialConvolution(6 -> 3, 3x3, 1,1, 1,1) without bias
(4): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(5): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(6): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(7): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(8): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(9): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
(10): nn.SpatialConvolution(3 -> 3, 3x3, 1,1, 1,1) without bias
}
Processing epoch: 1 , batch: 1
Processing epoch: 1 , batch: 2
Processing epoch: 1 , batch: 3
Processing epoch: 1 , batch: 4
Processing epoch: 1 , batch: 5
Processing epoch: 1 , batch: 6
Processing epoch: 1 , batch: 7
Processing epoch: 1 , batch: 8
Processing epoch: 1 , batch: 9
Processing epoch: 1 , batch: 10
Processing epoch: 1 , batch: 11
Processing epoch: 1 , batch: 12
Processing epoch: 1 , batch: 13
Processing epoch: 1 , batch: 14
Processing epoch: 1 , batch: 15
Totally elapse time: 26.187878131866 seconds
可以看到,明显慢了很多. 另外一点, batchsize是平分的, 比如batchsize=64, 2个GPU, 那么每个32.
第一种方法是很早的写法了, 可以参考 : https://github.com/torch/cunn/pull/73
比较繁琐, 而且还要自己在每个batch训练好后, 进行 dpt:syncParameters()
一下. 而且还有其他的弊端.
后来, 他们对上面的方法进行了改进, https://github.com/torch/cunn/pull/189 简单来说就是把 dpt:syncParameters()
可以不用写了. dpt:syncParameters()
被嵌入到 updateOuput
中. 程序会自动判断, 如果没有显示调用 dpt:syncParameters()
, 会自动进行在训练下一批之前, 将参数同步到所有GPU上.
require 'optim'
require 'nn'
require 'cutorch'
require 'cunn'
require 'torch'
torch.setdefaulttensortype('torch.FloatTensor')
local conv_net = nn.Sequential()
local concat = nn.ConcatTable()
concat:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
concat:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(concat)
local jt = nn.JoinTable(2)
conv_net:add(jt)
conv_net:add(nn.SpatialConvolution(6,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:add(nn.SpatialConvolution(3,3,3,3,1,1,1,1):noBias())
conv_net:cuda()
local optimState = {
learningRate = 1,
momentum = 0,
}
num_epochs = 1
batchsize = 64
criterion = nn.MSECriterion():cuda()
input = torch.randn(1000,3,256,256):mul(100):floor():cuda()
target = torch.randn(1000,3,256,256):mul(100):floor():cuda()
num_takes = input:size(1)/batchsize
dpt = nn.DataParallelTable(1)
-- -- 使用DataParallelTable进行多GPU, 1表示在batch维度进行切分
-- for i = 1, 2 do
-- cutorch.setDevice(i)
-- net:add(conv_net:clone():cuda(), i) -- 使用第1,2个GPU
-- -- end
-- print(net)
-- cutorch.setDevice(1)
-- parameters, gradParameters = net:getParameters()
-- 没必要设置主GP
dpt:add(conv_net, {1,2})
print(dpt)
-- TRAINING:
local timer = torch.Timer()
for i = 1, num_epochs do
for bth = 1, num_takes do
local input_bt = input:narrow(1, (bth-1)*batchsize+1, batchsize)
local target_bt = target:narrow(1, (bth-1)*batchsize+1, batchsize)
print('Processing epoch: ',i,' , batch: ',bth)
-- net:syncParameters()
local function feval(net)
local params, gradParams = net:getParameters()
return params, function(x)
net:zeroGradParameters()
local output = net:forward(input_bt)
local err = criterion:forward(output, target_bt)
local gradOutput = criterion:backward(output, target_bt)
local gradInput = net:backward(input_bt, gradOutput)
return err, gradParams
end
end
local params_dpt, feval_dpt = feval(dpt)
optim.sgd(feval_dpt, params_dpt, optimState)
-- net:syncParameters() -- 这个可以省略了
end
end
print('Totally elapse time: '..timer:time().real..' seconds')
由于上面的写法都是用一个线程, 我们可以使得每个GPU分别用单独的线程进行操作
示例:
gpus = torch.range(1, opt.nGPU):totable()
dpt = nn.DataParallelTable(1, true)
:add(model,gpus)
:threads(function()
require 'nngraph' -- 如果用nngraph, 加上这行
local cudnn = require 'cudnn'
cudnn.fastest, cudnn.benchmark = true, true
end)
dpt.gradInput = nil -- 这个是为了省内存
model = dpt:cuda()
其他方法, 比如 如何保存 dpt的模型啊等等,
参考:https://github.com/soumith/imagenet-multiGPU.torch
最后还是那句话“退torch,保平安!”