在 GPU 上使用简单的矩阵向量积对 Theano 和 CNTK 进行基准测试

Benchmarking Theano and CNTK with a simple matrix-vector product on the GPU

我想比较 Theano 和 CNTK 在一个非常简单的任务上的性能:GPU 上的矩阵向量乘积。我正在使用 Theano 0.9.0 和 CNTK 2.0.

我只想测量设备上计算所消耗的时间,不包括从主机到设备的数据传输所用的时间,反之亦然。

我得到的结果是这样的: figure (timings theano vs cntk) (N为重复次数,矩阵大小D为10000)

问题 1:

在 CNTK 案例中,mat-vec 产品的第一次执行似乎包含了一些准备工作(编译计算图?)所用的时间。 在 CNTK 中有什么方法可以像在 Theano 案例中那样拆分准备和执行吗?

问题 2:

我习惯了Theano,但在CNTK 是全新的,所以我不太确定CNTK 代码是否等同于Theano 代码。 我特别不确定CNTK代码的for循环中的操作是否真的包含在设备中,因为prod.eval() returns a numpy.ndarray。我错过了什么吗?

用于测量计时的代码:

import numpy as np
import time

# theano
def test_matVecDot_theano(D, N):
    import theano
    import theano.tensor as T
    A_cpu = np.random.normal(size=[D,D]).astype(np.float32)
    x_cpu = np.random.normal(size=[D]).astype(np.float32)
    A_gpu = theano.shared(A_cpu)
    x_gpu = theano.shared(x_cpu)
    b_gpu = theano.shared(x_cpu)
    b_gpu_new = T.dot(A_gpu,x_gpu)
    fnc = theano.function(inputs=[], outputs=None, updates=[(b_gpu, b_gpu_new)], allow_input_downcast=True)
    tic = time.time()
    for i in range(N):
        fnc()
    toc = time.time()
    print("time_theano:",toc-tic)

# cntk
def test_matVecDot_CNTK(D, N):
    import cntk as C
    A_cpu = np.random.normal(size=[D,D]).astype(np.float32)
    x_cpu = np.random.normal(size=[D,1]).astype(np.float32)
    A_c = C.Parameter(init=A_cpu, dtype=np.float32)
    x_c = C.Parameter(init=x_cpu, dtype=np.float32)
    b_c = C.Parameter(init=x_cpu, dtype=np.float32)
    prod = C.times(A_c, x_c)
    tic = time.time()
    for i in range(N):
        b_c.value = prod.eval() # is this operation enclosed in the device?
    toc = time.time()
    print("time_cntk:",toc-tic)

简短的回答是否定的,操作未包含在设备上。下面是发生的情况:当您调用 eval() 时,调用转到 C++,如果可能,它会在设备上执行操作。从 C++ 出来时,CNTK 检查 as_numpy 关键字参数的值是否默认为 True。当 as_numpy 为 True 时,gpu 缓冲区被急切地复制到 NumPy 数组中。

如果您调用 prod.eval(as_numpy=False),那么对 eval 的调用将不会将 gpu 缓冲区转换为 NumPy 数组。如果您将结果分配给一个普通的旧变量,您可以看到您获得了一个 CNTK Value 对象。但是在您的代码中,您分配给 b_c.value 属性。此作业由 value 属性 的 setter 处理(因为这个答案有点过于技术性,为了其他读者,我将 this link 包括在内)。 CNTK 在设备上执行此分配,尽管很难说。这是因为如果您尝试检查 b_c.value 如果您正在调用 .value 属性 getter 这将为您提供一个 NumPy 数组。所以看起来结果是一个 NumPy 数组,但这只是使用 b_c.value 的结果。任何其他变量都会让您看到它是一个 CNTK 值对象。同样,所有这些都适用于您执行 eval(as_numpy=False).

时的情况

此外,CNTK 使用时间戳,因此上述评估仅在 GPU 上发生一次。所有后续 N-1 对 eval() 的调用只会 return 相同的值对象(尽管每次都会发生向 Numpy 的转换,除非您指定 as_numpy=False.

最后,我不希望从这个基准中学到很多有意义的经验教训:CNTK 和 Theano 都调用相同的 CuDNN 实现,CNTK 的优势更多地围绕更高层次的东西,例如 (a) 带有一个高级库 (b) 除了一些专门的操作外,用户不必担心批处理和序列轴 (c) 高效的循环网络 (d) 高效 i/o (e) 易于分布式训练。

然后回答你关于设置时间的问题:我的理解是如果你只评估一次函数,就会编译它。 CNTK 实际上有两种编译方式:如果你只是 eval 第一次,它会编译前向传播。如果您稍后执行 function.grad ,它将丢弃 eval 编译并再次编译它,以便它可以处理前向和后向传递。