《第八章》

8.1命令式和符号式混合编程

命令式:jupyter lab上一边输入一边输出。

符号式:将所有代码转化为字符prog,然后通过下述方法执行。

y = compile(prog, '', 'exec')
exec(y)

混合式编程:1.使用HybridSequential;2.使用HybridBlock,如下。

class HybridNet(nn.HybridBlock):
    def __init__(self, **kwargs):
        super(HybridNet, self).__init__(**kwargs)
        self.hidden = nn.Dense(10)
        self.output = nn.Dense(2)

    # 是hybrid_forward而不是forward,且需要F来决定使用哪个类
    # MXNet有基于命令式编程的NDArray类(默认)和基于符号式编程的Symbol类。
    def hybrid_forward(self, F, x):
        print('F: ', F)
        print('x: ', x)
        x = F.relu(self.hidden(x))
        print('hidden: ', x)
        return self.output(x)

net = HybridNet()
net.initialize()
x = nd.random.normal(shape=(1, 4))
net.hybridize() # 能提升性能。
net(x)
net(x)
# 在hybrid_forward函数里,相同输入和中间输出全部变成了Symbol类型,再次前向后不再打印输出。
# 对于原地操作a += b和a[:] = a + b(需改写为a = a + b)

练习

  • 在本节HybridNet类的hybrid_forward函数中第一行添加x.asnumpy(),运行本节的全部代码,观察并分析报错的位置和错误类型。

    答:在转换成符号式编程net.hybridize()的时候提示

    Function asnumpy is not implemented for Symbol and only available in NDArray.

    因为对于少数像asnumpy这样的Symbol所不支持的函数无法在hybrid_forward函数中使用并在调用hybridize函数后进行前向计算。

  • 如果在hybrid_forward函数中加入Python的iffor语句会怎么样?

    加入if的错误:Function __bool__ (namely operator "bool") is not implemented for Symbol and only available in NDArray.

    加入for,暂时没发现错误。

  • 回顾前面几章中你感兴趣的模型,改用HybridBlock类或HybridSequential类实现。

8.2异步计算

class Benchmark():  # 本类已保存在d2lzh包中方便以后使用
    def __init__(self, prefix=None):
        self.prefix = prefix + ' ' if prefix else ''

    def __enter__(self):
        self.start = time.time()

    def __exit__(self, *args):
        print('%stime: %.4f sec' % (self.prefix, time.time() - self.start))

# 通过这个基准类来测试时间。     
with Benchmark('Workloads are queued.'):
    x = nd.random.uniform(shape=(2000, 2000))
    y = nd.dot(x, x).sum()
  • MXNet包括用户直接用来交互的前端和系统用来执行计算的后端。

  • MXNet能够通过异步计算提升计算性能。

  • 在“使用异步计算提升计算性能”一节中,我们提到使用异步计算可以使执行1000次计算的总耗时降为t1+1000t2+t3。这里为什么要假设1000t2>999t1?

    答:当执行真正计算的时间t2足够大的时候,才有必要通过异步降低(这题猜的)

8.3自动并行计算

MXNet能够通过自动并行计算提升计算性能。例如一边计算一边传入内存。

练习

  • 本节中定义的run函数里做了10次运算。它们之间也没有依赖关系。设计实验,看看MXNet有没有自动并行执行它们。

    答:有依赖,没有并行执行。

  • 设计包含更加复杂的数据依赖的计算任务,通过实验观察MXNet能否得到正确的结果并提升计算性能。

    答:可以。

  • 当运算符的计算量足够小时,仅在CPU或单块GPU上并行计算也可能提升计算性能。设计实验来验证这一点。

    def run(x):
        with d2l.Benchmark('Run.'):
            list =  [nd.dot(x, x) for _ in range(10)]
        return list
    x_cpu = nd.random.uniform(shape=(20, 20))
    x_gpu = nd.random.uniform(shape=(20, 20), ctx=mx.gpu(0))
    run(x_cpu)  
    run(x_gpu)
    nd.waitall()  
    ——————————结果——————————————
    Run. time: 0.0006 sec
    Run. time: 0.0000 sec

8.4多GPU计算

# 可以把各块显卡的显存上的数据加起来,然后再广播到所有的显存上。
def allreduce(data):
    for i in range(1, len(data)):
        data[0][:] += data[i].copyto(data[0].context)
    for i in range(1, len(data)):
        data[0].copyto(data[i])
# 将data平摊到ctx上
def split_and_load(data, ctx):
    n, k = data.shape[0], len(ctx)
    m = n // k  # 简单起见,假设可以整除
    assert m * k == n, '# examples is not divided by # devices.'
    return [data[i * m: (i + 1) * m].as_in_context(ctx[i]) for i in range(k)]
# 多GPU的小批量训练
def train_batch(X, y, gpu_params, ctx, lr):
    # 当ctx包含多块GPU及相应的显存时,将小批量数据样本划分并复制到各个显存上
    gpu_Xs, gpu_ys = split_and_load(X, ctx), split_and_load(y, ctx)
    with autograd.record():  # 在各块GPU上分别计算损失
        ls = [loss(lenet(gpu_X, gpu_W), gpu_y)
              for gpu_X, gpu_y, gpu_W in zip(gpu_Xs, gpu_ys, gpu_params)]
    for l in ls:  # 在各块GPU上分别反向传播
        l.backward()
    # 把各块显卡的显存上的梯度加起来,然后广播到所有显存上
    for i in range(len(gpu_params[0])):
        allreduce([gpu_params[c][i].grad for c in range(len(ctx))])
    for param in gpu_params:  # 在各块显卡的显存上分别更新模型参数
        d2l.sgd(param, lr, X.shape[0])  # 这里使用了完整批量大小
# 完整的训练函数
def train(num_gpus, batch_size, lr):
    train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
    ctx = [mx.gpu(i) for i in range(num_gpus)]
    print('running on:', ctx)
    # 将模型参数复制到num_gpus块显卡的显存上
    gpu_params = [get_params(params, c) for c in ctx]
    for epoch in range(4):
        start = time.time()
        for X, y in train_iter:
            # 对单个小批量进行多GPU训练
            train_batch(X, y, gpu_params, ctx, lr)
            nd.waitall()
        train_time = time.time() - start

        def net(x):  # 在gpu(0)上验证模型
            return lenet(x, gpu_params[0])

        test_acc = d2l.evaluate_accuracy(test_iter, net, ctx[0])
        print('epoch %d, time %.1f sec, test acc %.2f'
              % (epoch + 1, train_time, test_acc))
train(num_gpus=1, batch_size=256, lr=0.2) # 调用,输入gpu数量即可

练习

  • 在多GPU训练实验中,使用2块GPU训练并将batch_size翻倍至512,训练时间有何变化?如果希望测试准确率与单GPU训练中的结果相当,学习率应如何调节?

    答:考虑ctx = [mx.gpu(0),mx.cpu(0)]来模拟两个gpu的效果。这种方法十分耗费时间,我这边测试44s一个周期。1.训练时间差不多。2.单GPU中lr为0.17,GPU+CPU中lr为0.2

    (对于上述实验结果,持有迷惑行为,无力解释)

  • 将实验的模型预测部分改为用多GPU预测。

    # 偷看了9.1图像增广的答案
    # 本函数已保存在d2lzh包中方便以后使用
    def evaluate_accuracy(data_iter, net, ctx=[mx.cpu()]):
        if isinstance(ctx, mx.Context):
            ctx = [ctx]
        acc_sum, n = nd.array([0]), 0
        for batch in data_iter:
            features, labels, _ = _get_batch(batch, ctx)
            for X, y in zip(features, labels):
                y = y.astype('float32')
                acc_sum += (net(X).argmax(axis=1) == y).sum().copyto(mx.cpu())
                n += y.size
            acc_sum.wait_to_read()
        return acc_sum.asscalar() / n

8.5多GPU简洁计算

  1. 定义模型,初始化参数

  2. 划分数据样本到各个内存或显存上。

  3. 训练函数

  4. 训练

练习

  • 本节使用了ResNet-18模型。试试不同的迭代周期、批量大小和学习率。如果条件允许,使用更多GPU来计算。

    答:不想试,没什么意义。

  • 有时候,不同设备的计算能力不一样,例如,同时使用CPU和GPU,或者不同GPU之间型号不一样。这时候,应该如何将小批量划分到内存或不同显卡的显存?

    # 定义上下文
    ctx = [mx.gpu(0),mx.cpu(0)]
    # 修改后的训练函数:不要传入gpu数量,而是传入ctx
    def train(ctx, batch_size, lr):
        train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
        print('running on:', ctx)
        net.initialize(init=init.Normal(sigma=0.01), ctx=ctx, force_reinit=True)
        trainer = gluon.Trainer(
            net.collect_params(), 'sgd', {'learning_rate': lr})
        loss = gloss.SoftmaxCrossEntropyLoss()
        for epoch in range(4):
            start = time.time()
            for X, y in train_iter:
                gpu_Xs = gutils.split_and_load(X, ctx)
                gpu_ys = gutils.split_and_load(y, ctx)
                with autograd.record():
                    ls = [loss(net(gpu_X), gpu_y)
                          for gpu_X, gpu_y in zip(gpu_Xs, gpu_ys)]
                for l in ls:
                    l.backward()
                trainer.step(batch_size)
            nd.waitall()
            train_time = time.time() - start
            test_acc = d2l.evaluate_accuracy(test_iter, net, ctx[0])
            print('epoch %d, time %.1f sec, test acc %.2f' % (
                epoch + 1, train_time, test_acc))

最后更新于

这有帮助吗?