代码优化与程序加速指南——针对数值优化和深度学习领域

科技资讯 投稿 7400 0 评论

代码优化与程序加速指南——针对数值优化和深度学习领域

背景


本文旨在为程序加速提供一些代码方面的优化思路,通过优化代码结构、设计高性能计算方案,来有效加速程序运行,提高程序运行效率。需要注意的是,本文只涉及代码层面的加速方案,不包括算法、硬件等方面的优化措施。文章的撰写基于个人经验,如果有不足之处,敬请指出。

代码优化


1 并行化

程序优化的另一种思路是通过并行化加速程序。并行化需要软件与硬件配合,但前提是总任务能够被分解为同时进行的子任务。并行化有两种方式,一种依靠多核CPU实现多进程操作,一种依靠GPU完成。下面我们将分别介绍这两种方式的实现方法。

1.1 CPU多进程操作

1.1.1 多进程

当我们在电脑上运行程序时,实际上是启动了一个进程。进程是指在操作系统中运行的一个程序,它占据着系统的一些资源,如内存、CPU时间等。在传统的单进程计算模型中,所有的任务都在同一个进程中执行,如果任务需要进行复杂的计算,就会耗费大量时间,且无法利用多核CPU的优势。而多进程技术可以将任务分解为多个子任务,每个子任务在一个独立的进程中执行,从而实现并行计算,提高了计算效率。

1.1.2 多进程的Python实现

这是multiprocessing库的代码框架

import multiprocessing

def worker(num:
    """子进程要执行的任务"""
    print(f"Worker {num} is running"
    return

if __name__ == "__main__":
    processes = []
    num_processes = 4

    # 创建多个子进程
    for i in range(num_processes:
        p = multiprocessing.Process(target=worker, args=(i,
        processes.append(p
        p.start(

    # 等待所有子进程完成
    for p in processes:
        p.join(

    print("All workers are done"

在这个例子中,我们首先定义了一个worker函数,它是每个子进程要执行的任务。然后在主进程(if name=="main")中,我们创建了num_processes个子进程,并将它们添加到processes列表中。接着,我们遍历processes列表,用Process类中的start(方法启动每个子进程,并等待它们完成。最后输出"All workers are done",表示所有子进程都已经执行完毕。

1.2 GPU

相比于CPU,GPU有着更多的计算核心和更高的计算能力,可以更好地支持并行化计算。因此,利用GPU进行代码优化可以大幅度提高程序的运行效率。而要在Python中使用GPU进行代码优化,则需要使用GPU编程框架,比较常见的有NVIDIA开发的CUDA框架以及OpenCL框架。

1.2.1 GPU并行计算的Python实现

以下是一个简单的利用CUDA框架实现向量加法的例子:

import numpy as np
from numba import cuda

# 定义向量加法函数
@cuda.jit
def vector_add(a, b, c:
    i = cuda.grid(1
    if i < len(c:
        c[i] = a[i] + b[i]

# 定义主程序
if __name__ == '__main__':
    # 定义向量大小
    n = 100000

    # 在主机上生成随机向量
    a = np.random.randn(n.astype(np.float32
    b = np.random.randn(n.astype(np.float32
    c = np.zeros(n, dtype=np.float32

    # 将数据传输到GPU显存中
    d_a = cuda.to_device(a
    d_b = cuda.to_device(b
    d_c = cuda.to_device(c

    # 定义线程块和线程数量
    threads_per_block = 64
    blocks_per_grid = (n + (threads_per_block - 1 // threads_per_block

    # 执行向量加法操作
    vector_add[blocks_per_grid, threads_per_block](d_a, d_b, d_c

    # 将结果从GPU显存中传输回主机内存
    d_c.copy_to_host(c

    # 打印结果
    print(c

在上述代码中,我们首先定义了一个vector_add函数,用于将两个向量相加并将结果存储在第三个向量中。然后我们生成了两个随机向量,并将其传输到GPU显存中。接着,我们定义了线程块和线程数量,并在GPU部分,我们需要注意的是代码的向量化,也就是多利用矩阵运算编写代码,而不是多重for loop的串行计算。这样可以充分利用GPU的并行计算能力,加速神经网络的训练过程。例如,可以使用cuBLAS库来实现矩阵乘法,使用cuDNN库来实现卷积操作等。执行了向量加法操作。最后,我们将结果从GPU显存中传输回主机内存,并打印出结果。

1.2.2利用GPU训练神经网络

当涉及到训练深度神经网络时,GPU可以发挥其优势,因为神经网络的训练往往需要进行大量的矩阵运算,这正是GPU擅长的任务。为了使用GPU进行深度神经网络的训练,需要利用一些特定的框架和库,比如TensorFlow和PyTorch。
在利用CUDA框架进行神经网络训练时,首先,需要定义一个在GPU上运行的张量(tensor)来存储神经网络的参数。然后,将数据加载到GPU内存中,并将计算任务分配到GPU核心上。
下面是一个简单的基于PyTorch和CUDA的神经网络训练代码示例:

import torch
import torch.nn as nn
import torch.optim as optim

# 定义神经网络模型
class Net(nn.Module:
    def __init__(self:
        super(Net, self.__init__(
        self.conv1 = nn.Conv2d(3, 6, 5
        self.pool = nn.MaxPool2d(2, 2
        self.conv2 = nn.Conv2d(6, 16, 5
        self.fc1 = nn.Linear(16 * 5 * 5, 120
        self.fc2 = nn.Linear(120, 84
        self.fc3 = nn.Linear(84, 10

    def forward(self, x:
        x = self.pool(F.relu(self.conv1(x
        x = self.pool(F.relu(self.conv2(x
        x = x.view(-1, 16 * 5 * 5
        x = F.relu(self.fc1(x
        x = F.relu(self.fc2(x
        x = self.fc3(x
        return x

# 定义训练数据和标签
inputs = torch.randn(1, 3, 32, 32
labels = torch.randn(1, 10

# 将神经网络模型移动到GPU上
net = Net(.cuda(

# 将训练数据和标签移动到GPU上
inputs = inputs.cuda(
labels = labels.cuda(

# 定义损失函数和优化器
criterion = nn.MSELoss(
optimizer = optim.SGD(net.parameters(, lr=0.01

# 开始训练
for epoch in range(100:
    optimizer.zero_grad(
    outputs = net(inputs
    loss = criterion(outputs, labels
    loss.backward(
    optimizer.step(
    print('Epoch %d, Loss: %.4f' % (epoch+1, loss.item(

在上面的代码中,我们通过xxx.cuda(的方法将模型、训练数据和标签都加载到GPU内存中,从而使得训练循环中涉及的运算全部在GPU上完成。

1.2.3 注意事项

这是因为,GPU相较于CPU只是强在并行上,但是计算能力是差于CPU的,多层for循环这样的串行计算并不适合GPU。用沐神的话说,如果把CPU比作是一个大学生的话,GPU就像是一群小学生。大学生可以做微积分这样的任务,小学生只能做加减乘除。但如果把一个微积分这样的任务分解成多个加减乘除,那么一群小学生这样的GPU的优势就体现出来了。

\[L_\theta=-\sum_{i=1}^{N}\sum_{j=1}^{N_i}w_{ij}log(p(x_i^j|M \]

如果没有刻意注意代码的向量化,那么很容易想到的思路是这样的:

	def lossFunc(y_pred,sols,objs:
	batch size=y_pred.shape[0]
	loss=torch.tensor(0.0
	for i in range(batch_size:
		nSols=sols[i].shape[0] #当前batch(MIP下的可行解数目
		nVars=sols[i].shape[1]
		#目标断数归-化
		den=objs[i].sum(
		for l in range(objs[i].shape[0]:
			objs[i][l]=objs[i][l]/den
		den=sum(exp(-objs[i]#计算wii系数的分母
		sum1=torch.tensor(0.0
		for j in range(nSols:
			#计算权重wij
			w=exp(-objs[il[j]/den
			#计算可行解生成的概率
			P=torch.tensor(1.9
			for k in range(nVars:
				if sols[il[j,k]==1:
					P=p*y_pred[i][k]
				elif sols[il[j,k]==0:
					P=p*(1-y_pred[i][k]
			#计算求和
			sum1+=w*p
		loss+=sum1
	loss=-loss
	return loss

但实际上用多重for循环设计的损失函数并不利于在GPU上工作,甚至比不上在CPU上执行这样的代码。最后测试结果发现如果使用这样的损失函数,一个epoch大约跑1个小时....所以也不可能把神经网络训练出来了。

def lossFunc(y,sols,objs:
    """
    损失函数
    Parameters
    ----------
    y : 神经网络输出 batch_size x nVars
    sols : 可行解集合 batch_size x nSols x nVars
    objs : 可行解对应的目标函数值 batch_size x nSols

    Returns
    -------
    loss : 在当前batch上的损失
    """
    objs=objs/15
    eObjs=exp(-objs
    den=eObjs.sum(axis=1
    den=den.unsqueeze(1
    w=eObjs/den
    y=y.unsqueeze(1
    p=y*sols+(1-y*(1-sols
    p=log(p+1e-45
    P=p.sum(axis=2
    loss=-(w*P.sum(
    return loss

所以正确的写法应该是这样的,不仅在GPU上加速效果明显,并且看起来简洁多了...但是缺点就是,得不断利用矩阵运算的广播机制,并且最好在小规模的例子上多手算几遍,边算边设计。

2 编译器加速

2.1 原理

编译器优化的方式有很多种,其中最常见的方式包括:

  1. 循环展开:循环展开是指将循环体中的代码重复执行几次,从而减少循环的次数。这样可以提高程序的运行速度。

为了帮助开发者更加方便地利用编译器优化代码,一些开源JIT编译器如Numba被开发出来。这些编译器可以将Python等解释型语言代码转换为可执行的机器代码,从而提高程序的运行速度。

2.2 基于Numba的Python加速方案

下面是一个使用Numba实现代码加速的大致代码框架:

    导入numba库
  1. 定义一个需要优化的函数
  2. 使用 @numba.jit 装饰器对函数进行装饰,生成numba优化后的函数
  3. 调用优化后的函数

假设我们有一个多重嵌套的for循环,用于计算一个矩阵的行列式。这是一个非常计算密集的操作,可以使用Numba进行加速。

def det(matrix:
    n = len(matrix
    if n == 1:
        return matrix[0][0]
    elif n == 2:
        return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]
    else:
        result = 0
        for j in range(n:
            sub_matrix = []
            for i in range(1, n:
                row = []
                for k in range(n:
                    if k != j:
                        row.append(matrix[i][k]
                sub_matrix.append(row
            result += matrix[0][j] * det(sub_matrix * (-1 ** j
        return result

可以看到,这个函数包含了多重嵌套的for循环,非常容易受到Python解释器的性能限制。现在我们使用Numba进行加速。

import numba

@numba.jit(nopython=True
def det(matrix:
    n = len(matrix
    if n == 1:
        return matrix[0][0]
    elif n == 2:
        return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]
    else:
        result = 0
        for j in range(n:
            sub_matrix = np.zeros((n-1, n-1
            for i in range(1, n:
                for k in range(n:
                    if k != j:
                        sub_matrix[i-1, k-(k>j] = matrix[i, k]
            result += matrix[0][j] * det(sub_matrix * (-1 ** j
        return result

我们使用了@numba.jit(nopython=True装饰器,将函数声明为Numba可加速的函数。同时,我们使用了Numpy数组代替了Python的列表,并使用Numpy数组的切片操作和广播功能,以减少循环和内存分配。

import time
import numpy as np

# Generate a random 10x10 matrix
matrix = np.random.rand(10, 10

# Time the original Python code
start = time.time(
d = det(matrix
end = time.time(
print(f"Python code took {end-start:.4f} seconds, result={d}"

# Time the Numba-optimized code
start = time.time(
d = det(matrix
end = time.time(
print(f"Numba-optimized code took {end-start:.4f} seconds, result={d}"

测试结果:

Python code took 0.5960 seconds, result=-0.004127521725273144
Numba-optimized code took 0.0040 seconds, result=-0.004127521725273144

使用Numba优化Python代码的大致流程如上所述。对于需要优化的函数,我们可以通过添加numba.jit 装饰器实现代码优化,该装饰器会自动进行类型推导并将Python代码编译为机器码。

此外,还需要注意的是,Numba在进行代码优化时,会将Python代码转换为LLVM IR(Intermediate Representation,中间表示形式),这一过程可能会导致代码的可读性降低。因此,在使用Numba进行优化时,需要仔细考虑代码可读性和可维护性的平衡。

总结


值得一提的是,这篇技术博客是在chatGPT的帮助下共同完成的。作者提供了大致的写作思路和要点,而主要内容和其中的代码案例均由chatGPT构建。我们相信,本篇博客对于想要提高代码性能和效率的程序员们会有所帮助。

附图

编程笔记 » 代码优化与程序加速指南——针对数值优化和深度学习领域

赞同 (26) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽