cuda与机器学习框架
1.基础概述
CUDA是由NVIDIA开发的并行计算平台和编程模型,允许开发者利用支持CUDA的NVIDIA GPU来加速计算密集型任务。CUDA提供了扩展的C/C++语言,以及用于在GPU上执行并行计算的API。
线程、线程块、网格、束
-
线程
- 基本执行单元:在 GPU 中,线程是最小的执行单元,负责执行 CUDA 内核函数中的指令。
特点
- 轻量级:GPU 线程相比 CPU 线程更加轻量级,创建和切换开销小。
- 大量存在:一次可以有成千上万的线程同时存在,发挥 GPU 的并行计算能力。
- 独立执行:每个线程都有自己的寄存器和本地内存,可以独立执行。
- 线程索引:每个线程可以通过内置变量获取自己的线程索引,如
threadIdx.x
。
-
线程块
-
线程的集合:线程块是多个线程的集合,组成一个可在 GPU 上执行的基本调度单元。
特点
- 共享内存:同一线程块内的线程可以共享快速的片上共享内存,支持线程间的数据交换。
- 同步机制:线程块内的线程可以使用
__syncthreads()
等同步指令进行同步。 - 最大尺寸:线程块的大小受 GPU 架构的限制,通常是 1D、2D 或 3D 的维度。
-
线程块索引:通过
blockIdx.x
、blockIdx.y
、blockIdx.z
获取线程块的索引。 -
线程块大小:通过
blockDim.x
、blockDim.y
、blockDim.z
获取线程块的维度大小。 -
全局线程索引:结合线程块索引和线程索引,可以计算线程在整个网格中的全局索引。
int global_thread_id = blockIdx.x * blockDim.x + threadIdx.x;
- 网格
- 线程块的集合:网格是线程块的集合,用于组织和管理 GPU 上的大规模并行计算。
特点
- 独立执行:不同的线程块可以独立执行,之间不能直接通信。
- 尺寸灵活:网格可以是一维、二维或三维,方便映射复杂的数据结构。
- 网格大小:通过
gridDim.x
、gridDim.y
、gridDim.z
获取网格的维度大小。
总结
*线程(Thread):GPU 的最小执行单元,具有自己的寄存器和本地内存。
线程块(Thread Block):由多个线程组成,同一块内的线程可以共享内存和同步。
网格(Grid):由多个线程块组成,用于组织大规模并行计算。
warp(束)
执行单元:在实际执行中,GPU 将线程块中的线程分成若干个 Warp,每个 Warp 通常包含 32 个线程。
SIMT 模式:Warp 采用单指令多线程(SIMT)的执行模式,Warp 内的线程同时执行相同的指令,但操作不同的数据。
控制流分歧:如果 Warp 内的线程执行不同的分支,会导致性能下降(称为分支发散)。
内存
在CUDA编程和GPU计算的上下文中,**主机内存(Host Memory)和设备内存(Device Memory)**是指两个不同的内存空间,分别由CPU和GPU管理和访问。
1. 主机内存(Host Memory)
定义: 主机内存是指计算机系统中的主存储器,即RAM(随机存取存储器)。这部分内存由CPU管理和使用,用于存储CPU执行程序时需要的数据和指令。
特点:
- CPU直接访问: CPU可以直接读取和写入主机内存中的数据。
- 与GPU的关系: GPU无法直接访问主机内存中的数据。如果需要GPU处理主机内存中的数据,必须先将数据传输到设备内存。
2. 设备内存(Device Memory)
定义: 设备内存是指GPU上的内存资源,包括全局内存、共享内存、常量内存和纹理内存等。设备内存由GPU管理,用于存储GPU在执行并行计算时需要的数据。
三种内存类型的比较
特性 全局内存 共享内存 常量内存 容量 大(数 GB) 小(几十 KB) 小(64 KB) 延迟 高(数百个周期) 低(几个周期) 低(缓存命中时) 可访问性 所有线程 同一线程块内的线程 所有线程(只读) 读写权限 读写 读写 只读 用途 存储大规模数据 线程块内数据共享 存储常量数据 优化重点 合并访问、减少次数 合理划分、同步 统一访问地址 特点:
- GPU直接访问: GPU内的各个计算核心(CUDA核心)可以直接读取和写入设备内存。
- 与CPU的关系: CPU无法直接访问设备内存。如果CPU需要获取设备内存中的数据,必须将数据从设备内存传回主机内存。
3.主机内存与设备内存之间的关系
由于CPU和GPU各自有独立的内存空间,二者之间不能直接共享内存。因此,在CUDA编程中,需要通过显式的数据传输,将数据在主机内存和设备内存之间进行复制。这种数据传输通常是通过PCIe总线完成的。
#主机与设备之间的数据传输可以通过cudaMemcpy()实现,
cudaMemcpy(device_ptr, host_ptr, size, cudaMemcpyHostToDevice); #主机到设备
cudaMemcpy(host_ptr, device_ptr, size, cudaMemcpyDeviceToHost); #设备到主机
4.主机与设备之间数据传输代码示例
#include <cuda_runtime.h>
#include <iostream>__global__ void addOne(int* d_data) {int idx = threadIdx.x + blockIdx.x * blockDim.x;d_data[idx] += 1; // 设备内存中的数据加1
}int main() {const int arraySize = 10;int h_data[arraySize]; // 主机内存数据// 初始化主机内存数据for (int i = 0; i < arraySize; ++i) {h_data[i] = i;}int* d_data = nullptr; // 设备内存指针// 在设备内存中分配空间cudaMalloc((void**)&d_data, arraySize * sizeof(int));// 将数据从主机内存复制到设备内存cudaMemcpy(d_data, h_data, arraySize * sizeof(int), cudaMemcpyHostToDevice);// 启动核函数,在GPU上执行addOne<<<1, arraySize>>>(d_data);// 将结果从设备内存复制回主机内存cudaMemcpy(h_data, d_data, arraySize * sizeof(int), cudaMemcpyDeviceToHost);// 输出结果for (int i = 0; i < arraySize; ++i) {std::cout << h_data[i] << " "; // 应该输出1到10}std::cout << std::endl;// 释放设备内存cudaFree(d_data);return 0;
}
2.cuda 程序的执行
1.CUDA程序的编译和链接过程:
- NVCC编译器:
- CUDA程序通常使用
nvcc
(NVIDIA CUDA Compiler)编译器进行编译。nvcc
是一个编译器驱动程序,它调用其他编译器(如gcc
或cl.exe
)来编译主机代码,同时使用NVIDIA自己的工具链来编译设备代码(GPU上的代码)。
- 代码分离:
CUDA程序包含两种代码:
- 主机代码(Host Code): 在CPU上运行的代码,使用标准的C/C++语法。
- 设备代码(Device Code): 在GPU上运行的代码,使用CUDA扩展的C/C++语法,包含核函数定义。
- 编译过程:
- 预处理: 处理``#include
、
#define`等预处理指令。
主机代码编译:主机代码被提取并使用主机编译器(如
gcc
)编译为目标文件(.o文件)。设备代码编译:设备代码被提取并编译为PTX(并行线程执行,Parallel Thread Execution)中间表示,或者直接编译为GPU的机器代码(SASS,Streaming Assembly)。
设备代码嵌入:编译后的设备代码被嵌入到主机目标文件中,通常作为全局数组或特殊的段。
2.链接过程:
- 链接器将主机目标文件和库文件链接在一起,生成可执行文件。
- 嵌入的设备代码在运行时由CUDA驱动程序提取并加载到GPU。
2.核函数(Kernel)是如何发射到GPU上的:
1.核函数调用语法:
-
核函数在主机代码中通过特殊的语法调用:
kernelFunction<<<gridDim, blockDim, sharedMem, stream>>>(args);
gridDim
:线程网格的维度和尺寸。blockDim
:每个线程块的维度和尺寸。sharedMem
:可选参数,动态分配的共享内存大小。stream
:可选参数,指定CUDA流。
2.运行时处理:
- 核函数调用被翻译为对CUDA运行时库的调用,设置核函数执行配置。
- 运行时库将核函数的配置信息和参数打包,准备发送给CUDA驱动程序。
3.驱动程序与GPU通信:
- CUDA驱动程序负责与GPU通信,分配资源,管理上下文。
- 驱动程序将核函数代码和参数传递给GPU,指示GPU启动核函数执行。
3.CUDA程序执行时CPU端和GPU端各自的工作:
- CPU端的工作:
- 初始化:
- 配置CUDA环境,选择GPU设备。
- 分配主机内存,初始化数据。
- 内存管理:
- 使用
cudaMalloc()
在GPU上分配设备内存。 - 使用
cudaMemcpy()
将数据从主机复制到设备。
- 使用
- 核函数启动:
- 使用核函数调用语法启动GPU上的并行计算。
- CPU线程将执行命令发送到GPU,但不等待GPU完成。
- 继续执行或同步:
- CPU可以继续执行后续代码。
- 如果需要结果,CPU会调用
cudaDeviceSynchronize()
等待GPU完成计算。
- 数据传回与清理:
- 使用
cudaMemcpy()
从设备复制结果回主机。 - 释放设备内存,清理资源。
- 使用
- 初始化:
- GPU端的工作:
- 接受命令:
- GPU接收来自CPU的核函数执行命令。
- 线程调度与执行:
- 根据配置,GPU创建线程网格和线程块。
- 每个线程执行核函数代码,处理数据。
- 内存访问:
- 线程访问设备内存,包括全局内存、共享内存、常量内存等。
- 同步与通信:
- 线程块内的线程可以使用
__syncthreads()
进行同步。 - 不同线程块之间通过全局内存通信。
- 线程块内的线程可以使用
- 完成计算:
- 核函数执行结束,GPU通知驱动程序。
- 接受命令:
4. CPU和GPU之间的异步与同步:
- 异步执行:
- 核函数调用和数据传输可以是异步的,CPU无需等待操作完成。
- 这允许CPU和GPU并行工作,提高性能。
- 同步操作:
- 使用
cudaDeviceSynchronize()
,cudaStreamSynchronize()
等函数使CPU等待GPU完成。 - 使用CUDA事件(Event)进行精细的同步控制。
- 使用
5.内存管理与数据传输:
- 设备内存分配:
cudaMalloc(void** devPtr, size_t size)
:在GPU上分配内存。cudaFree(void* devPtr)
:释放GPU内存。
- 数据传输:
cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)
- 将数据在主机和设备之间复制。
cudaMemcpyHostToDevice
,cudaMemcpyDeviceToHost
,cudaMemcpyDeviceToDevice
等。
- 统一内存(Unified Memory):
cudaMallocManaged(void** devPtr, size_t size)
- 分配统一内存,主机和设备共享。
- 简化了内存管理,但可能影响性能。
6.cuda程序执行例子
#include <cuda_runtime.h>
#include <iostream>// 核函数:在GPU上运行的代码
__global__ void vectorAdd(const float* A, const float* B, float* C, int N) {int i = blockDim.x * blockIdx.x + threadIdx.x;if (i < N) {C[i] = A[i] + B[i];}
}int main() {int N = 1 << 20; // 1M元素size_t size = N * sizeof(float);// 分配主机内存float *h_A = (float*)malloc(size);float *h_B = (float*)malloc(size);float *h_C = (float*)malloc(size);// 初始化输入数据for (int i = 0; i < N; ++i) {h_A[i] = static_cast<float>(i);h_B[i] = static_cast<float>(i * 2);}// 分配设备内存float *d_A = nullptr;float *d_B = nullptr;float *d_C = nullptr;cudaMalloc((void**)&d_A, size);cudaMalloc((void**)&d_B, size);cudaMalloc((void**)&d_C, size);// 将数据从主机复制到设备cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);// 定义线程块和线程网格的尺寸int threadsPerBlock = 256;int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;// 启动核函数vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);// 等待GPU完成cudaDeviceSynchronize();// 将结果从设备复制回主机cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);// 验证结果for (int i = 0; i < N; ++i) {if (fabs(h_C[i] - (h_A[i] + h_B[i])) > 1e-5) {std::cerr << "Result verification failed at element " << i << std::endl;exit(EXIT_FAILURE);}}std::cout << "Test PASSED" << std::endl;// 释放设备内存cudaFree(d_A);cudaFree(d_B);cudaFree(d_C);// 释放主机内存free(h_A);free(h_B);free(h_C);return 0;
}
编译:
nvcc -o vectorAdd vectorAdd.cu
-
nvcc
处理主机代码和设备代码的编译,并生成可执行文件。 -
核函数发射:
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
- 这行代码在GPU上启动核函数,进行并行计算。
-
CPU的工作:
- 分配并初始化主机内存。
- 分配设备内存,复制数据到设备。
- 启动核函数,等待GPU完成。
- 复制结果回主机,验证结果。
- 释放内存,结束程序。
-
GPU的工作:
- 执行核函数
vectorAdd
,每个线程计算一个元素的和。 - 将结果存储在设备内存
d_C
中。
- 执行核函数
7. CPU和GPU在CUDA程序执行时的协同:
- 并行性:
- CPU和GPU可以并行执行各自的任务。
- 需要注意同步点,确保数据的一致性。
- 同步机制:
- 全局同步:
cudaDeviceSynchronize()
使CPU等待GPU完成所有任务。 - 流同步:
cudaStreamSynchronize(stream)
等待指定流中的任务完成。 - 事件同步: 使用
cudaEvent_t
进行更精细的同步控制。
- 全局同步:
- 数据依赖:
- 在启动新的核函数或进行数据传输前,确保之前的操作已完成,避免数据竞争。
8. 总结:
- 编译和链接:
- 使用
nvcc
编译器,处理主机和设备代码。 - 主机代码被编译为CPU可执行的指令。
- 设备代码被编译为GPU可执行的指令,嵌入到主机代码中。
- 使用
- 核函数发射:
- 通过特殊的语法调用核函数,配置线程布局。
- CUDA运行时和驱动程序负责将核函数加载并执行在GPU上。
- 程序执行时的工作分配:
- CPU:
- 控制程序流程,管理内存,启动核函数,处理结果。
- GPU:
- 执行并行计算,加速数据处理。
- CPU:
- 内存管理:
- 主机和设备各自管理自己的内存空间。
- 使用CUDA API在两者之间传输数据。
- 同步与通信:
- 使用CUDA提供的同步机制,确保CPU和GPU协同工作。
CUDA程序能够充分利用GPU的并行计算能力,加速计算密集型任务。在编写CUDA程序时,需要关注内存管理、线程配置和同步机制,以确保程序的正确性和高效性。
CUDA 官方文档:https://docs.nvidia.com/cuda/
CUDA 编程指南:https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
CUDA 教程:https://developer.nvidia.com/cuda-education
3.常用机器学习框架集成
1.TensorFlow
- TensorFlow是由Google开发的一个开源机器学习框架,主要用于深度学习模型的构建、训练和部署。最初由Google Brain团队为研究和生产环境而创建,TensorFlow于2015年11月正式开源。它提供了一个灵活且全面的生态系统,包括各种工具、库和社区资源,支持研究人员和开发者轻松创建和部署先进的机器学习应用。
TensorFlow使用了自定义的GPU操作符,并通过调用cuDNN和cuBLAS等库来加速计算。当在GPU上运行时,TensorFlow会自动将计算图中的操作分配给可用的GPU设备。
主要特点:
- 多平台支持:TensorFlow可以在CPU、GPU和TPU上运行,支持从桌面设备到服务器集群以及移动设备的部署。
- 易于使用的API:提供了高级的Keras接口,使模型构建更加简洁明了,同时也支持低级API以满足高级用户的需求。
- 灵活的架构:采用符号式和命令式编程相结合的方式,既支持静态计算图又支持动态图,使开发者能够根据需求选择合适的编程模型。
- 丰富的生态系统:
- TensorFlow Hub:共享和复用机器学习模型的平台。
- TensorFlow Lite:用于移动和嵌入式设备的轻量级解决方案。
- TensorFlow Extended (TFX):用于生产环境的端到端平台,涵盖数据验证、模型训练、服务等。
- 强大的社区支持:拥有全球活跃的开发者社区,丰富的教程、示例和第三方库。
应用领域:
- 计算机视觉:图像分类、目标检测、图像分割等。
- 自然语言处理:文本分类、机器翻译、文本生成等。
- 语音识别与合成:语音到文本转换、语音情感分析等。
- 强化学习:游戏AI、机器人控制等。
TensorFlow官方文档,https://www.tensorflow.org/
TensorFlow GitHub仓库,https://github.com/tensorflow/tensorflow
《TensorFlow Machine Learning Projects》,Packt Publishing,2018年。
2.PyTorch
PyTorch是一个开源的深度学习框架,由Facebook的人工智能研究团队(FAIR)于2016年发布。它基于Torch库,采用Python语言编写,支持GPU加速,广泛应用于学术研究和工业实践中。PyTorch以其动态计算图和Pythonic的编码风格,受到了广大开发者和研究人员的欢迎。
PyTorch提供了动态计算图,允许更灵活的操作。它使用了CUDA Tensor,开发者可以通过调用
.cuda()
方法,将张量和模型转移到GPU上。PyTorch底层同样调用了cuDNN和cuBLAS等库,以加速深度学习计算。
主要特点:
- 动态计算图(Dynamic Computational Graph):
- PyTorch采用了动态计算图机制,允许在运行时改变网络结构。这使得模型的构建和调试更加灵活,特别适合于需要动态变化的神经网络,如循环神经网络(RNN)和自然语言处理(NLP)任务。
- Pythonic风格:
- PyTorch的API设计非常贴近Python的编程习惯,代码简洁直观,易于上手。这降低了学习曲线,使开发者能够专注于模型本身。
- 强大的社区和生态系统:
- 拥有活跃的开源社区,提供了丰富的第三方库和工具,如torchvision(计算机视觉)、torchtext(自然语言处理)和torchaudio(音频处理)等。
- 自动微分机制:
- 内置了Autograd模块,可以自动计算张量的梯度,方便实现反向传播,简化了深度学习模型的训练过程。
- 多GPU和分布式训练:
- 支持在多GPU环境下进行模型训练,提供了分布式训练的接口(
torch.distributed
),可以加速大型模型的训练过程。
应用领域:
- 计算机视觉: 图像分类、目标检测、图像分割、风格迁移等。
- 自然语言处理: 机器翻译、文本生成、情感分析、问答系统等。
- 强化学习: 游戏AI、机器人控制策略等。
- 语音识别与合成: 语音转文字、语音生成等。
PyTorch官方文档:https://pytorch.org/docs/
PyTorch GitHub仓库:https://github.com/pytorch/pytorch
《深度学习框架PyTorch:入门与实践》,机械工业出版社,2019年。
3.MXNet
Apache MXNet是一个高性能、可扩展的开源深度学习框架,由DMLC(Distributed Machine Learning Community)开发,现为Apache软件基金会的顶级项目。MXNet支持多种编程语言,包括Python、R、Scala、Julia、C++、JavaScript和Perl,旨在提供灵活性和高效性,满足各种深度学习应用的需求。
MXNet: MXNet支持多种语言接口,如Python、R和Scala。它使用了名为NDArray的高级数据结构,支持CPU和GPU计算。通过简单地指定上下文(如
mx.gpu()
),可以将计算任务分配到GPU。
主要特点:
- 灵活的编程模型:
- 混合式编程(Hybrid Programming): MXNet支持命令式和符号式编程的结合。通过Gluon接口,开发者可以方便地定义、训练和部署模型,既享受命令式编程的灵活性,又具备符号式编程的高性能。
- 高性能和可扩展性:
- 高效计算: 针对GPU和CPU进行了高度优化,支持自动并行计算,加速模型训练和推理。
- 分布式训练: 原生支持多机多卡的分布式训练,能够处理大型数据集和复杂模型。
- 丰富的生态系统:
- Gluon接口: 一个简洁而强大的高级API,使得模型构建、训练和部署更加直观。
- 预训练模型库: 提供大量预训练模型,方便进行迁移学习和快速原型开发。
- 跨平台支持:
- 多语言支持: 除了Python,还支持R、Scala、Julia等多种语言,满足不同开发者的需求。
- 移动和嵌入式设备: 支持在移动设备和嵌入式系统上部署模型,如Android和iOS。
应用领域:
- 计算机视觉: 图像分类、目标检测、图像分割等。
- 自然语言处理: 机器翻译、文本生成、情感分析等。
- 语音识别与合成: 语音到文本转换、语音情感分析等。
- 推荐系统: 个性化推荐、协同过滤等。
Apache MXNet官方网站:https://mxnet.apache.org/
MXNet GitHub仓库:https://github.com/apache/mxnet
4.机器学习框架与CUDA的调用:
机器学习框架通常不会直接编写CUDA代码,而是使用高层次的库和API。这些框架利用了诸如cuDNN(CUDA Deep Neural Network library)和cuBLAS(CUDA Basic Linear Algebra Subprograms)等NVIDIA提供的深度学习和线性代数库。这些库针对GPU进行了高度优化,提供了常用操作的高效实现,如卷积、池化和矩阵乘法等。
- 并行计算: 通过CUDA,框架可以在GPU上并行执行大量计算。GPU擅长处理大规模的并行任务,例如对大型张量进行元素级操作。这使得深度学习模型的训练和推理速度显著提升。
- 自定义CUDA内核: 对于高级用户,某些框架允许编写自定义的CUDA内核,以优化特定的操作。例如,在TensorFlow中,可以使用CUDA代码来编写自定义的OP(操作符),以实现特殊的功能或优化性能。
- 设备抽象: 为了提高可移植性,这些框架通常提供了设备抽象层。开发者可以编写与设备无关的代码,框架会根据实际运行环境,将计算分配到CPU或GPU上。这使得代码能够在不同的硬件配置上运行,而无需修改。
- 优化与调优: 框架与CUDA的集成允许利用NVIDIA的性能分析和调优工具,如nvprof和Nsight。这些工具可以帮助开发者分析GPU的性能瓶颈,进行优化。
- 内存管理: 框架通常负责管理CPU和GPU之间的数据传输。它们提供了自动化的机制,将数据从主机内存复制到设备内存,反之亦然。这种内存管理对于优化性能至关重要,因为不必要的数据传输会导致性能下降。
1.tensorflow内部调用cuda的流程
1. TensorFlow 内部调用 CUDA 的理论介绍
1.1 设备抽象与管理
-
设备抽象:TensorFlow 使用
tf.Device
对象来抽象物理计算设备,包括 CPU 和 GPU。每个操作(Operation)可以指定在特定的设备上执行,如'/CPU:0'
或'/GPU:0'
。https://www.tensorflow.org/api_docs/python/tf/device
with tf.device('/job:foo'):# ops created here have devices with /job:foowith tf.device('/job:bar/task:0/device:gpu:2'):# ops created here have the fully specified device abovewith tf.device('/device:gpu:1'):# ops created here have the device '/job:foo/device:gpu:1'
-
设备枚举:在程序初始化时,TensorFlow 会枚举系统中可用的物理设备,创建相应的设备对象,供后续的计算图构建和执行。
1.2 操作和内核的实现
- 操作(Op):TensorFlow 中的操作是计算图的基本组成单元,例如矩阵乘法、卷积等。
- 内核(Kernel):每个操作都有针对不同设备的内核实现。对于 GPU,内核通常使用 CUDA 或者基于 CUDA 的库(如 cuDNN、cuBLAS)编写。
- 多版本内核:同一个操作可能有多个内核实现,以适应不同的设备和数据类型。TensorFlow 在执行时会根据设备和数据类型选择合适的内核。
1.3 CUDA 内核的调用过程
- 编译与加载:CUDA 内核代码通常在编译 TensorFlow 时就已经编译为二进制。运行时,TensorFlow 会加载这些预编译的 CUDA 内核。
- 调用流程:
- 准备数据:将需要计算的数据准备好,确保数据在 GPU 内存中。
- 设置参数:配置 CUDA 内核的参数,包括线程块大小、网格大小等。
- 启动内核:通过 CUDA 的 API,启动内核函数在 GPU 上执行。
- 同步与异步执行:TensorFlow 利用 CUDA 的流(Stream)和事件(Event)机制,支持异步计算和操作间的依赖。
1.4 内存管理与数据传输
- 内存分配:TensorFlow 使用 CUDA 的内存管理 API,如
cudaMalloc
、cudaFree
,在 GPU 上分配和释放内存。 - 数据传输:
- 主机到设备:当数据从 CPU 传输到 GPU 时,使用
cudaMemcpy
等函数。 - 设备到设备:在多个 GPU 之间传输数据,使用
cudaMemcpyPeer
等函数。 - 异步传输:为了提高效率,数据传输可以与计算并行,利用 CUDA 的异步拷贝功能。
- 主机到设备:当数据从 CPU 传输到 GPU 时,使用
1.5 图的优化与执行
- 计算图优化:TensorFlow 在执行前,会对计算图进行优化,包括节点融合、常量折叠、内存优化等。
- 调度与执行:
- 设备分配:根据操作的类型和设备的可用性,确定每个操作在何种设备上执行。
- 依赖管理:确保操作按依赖顺序执行,利用 CUDA 的事件机制实现同步。
- 利用库函数:TensorFlow 集成了 cuDNN、cuBLAS 等高度优化的库,在可能的情况下调用这些库函数以提高性能。
2.MINIST手写数字识别例子
2.1code:
import tensorflow as tf
from tensorflow.keras import datasets# 加载 MNIST 数据集
(train_images, train_labels), (test_images, test_labels) = datasets.mnist.load_data()
# 将图像数据重塑为 (num_samples, 28, 28, 1)
train_images = train_images.reshape((60000, 28, 28, 1))
test_images = test_images.reshape((10000, 28, 28, 1))
# 转换数据类型为 float32,这是 CUDA 计算所需的类型
train_images = train_images.astype('float32')
test_images = test_images.astype('float32')
# 归一化像素值
train_images /= 255.0
test_images /= 255.0
# 创建数据集对象
train_ds = tf.data.Dataset.from_tensor_slices((train_images, train_labels))
train_ds = train_ds.shuffle(60000).batch(64)
from tensorflow.keras import layers, models# 定义模型
model = models.Sequential([layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),layers.MaxPooling2D((2, 2)),layers.Flatten(),layers.Dense(64, activation='relu'),layers.Dense(10, activation='softmax')
])# 编译模型
model.compile(optimizer='adam',loss='sparse_categorical_crossentropy',metrics=['accuracy'])# 训练模型
model.fit(train_ds, epochs=5)
2.2训练过程中的数据流动
-
数据加载到内存:数据最初存储在主机内存(CPU 内存)中。
-
数据传输到 GPU:
- 批次加载:每次从数据集中取出一个批次的数据,大小为 64。
- 自动管理:TensorFlow 自动将需要的批次数据从主机内存复制到 GPU 内存,无需显式调用数据传输函数。
-
前向传播:
- 卷积层计算:调用 CUDA 内核或 cuDNN 库函数,在 GPU 上执行卷积操作。
- 激活函数:在 GPU 上执行非线性激活函数的计算。
- 池化层:利用 CUDA 内核在 GPU 上执行最大池化操作。
- 全连接层:矩阵乘法运算,调用 cuBLAS 库函数。
-
反向传播:
- 梯度计算:在 GPU 上计算损失函数对每个参数的梯度。
- 参数更新:根据优化算法(如 Adam),在 GPU 上更新模型参数。
-
数据转换与内存管理:
- 数据格式:在 GPU 上,数据通常以
NHWC
(批次、高度、宽度、通道)的格式存储,这与 TensorFlow 默认的数据格式一致,避免了数据格式转换的开销。 - 内存复用:TensorFlow 的运行时会优化内存使用,复用内存块,减少内存分配和释放的次数。
- 数据格式:在 GPU 上,数据通常以
-
多线程与异步执行:
- 数据预处理线程:数据加载和预处理可以在 CPU 上的多个线程中进行,避免阻塞 GPU 的计算。
- 异步计算:利用 CUDA 流,TensorFlow 可以在等待数据传输的同时执行其他计算,提高硬件利用率。
https://www.tensorflow.org/guide/gpu
https://www.tensorflow.org/guide/data
2.pytorch 内部调用cuda
1. PyTorch 内部调用 CUDA 的理论介绍
1.1张量和自动求导机制
- 张量(Tensor):PyTorch 的核心数据结构是张量,它类似于 NumPy 的 ndarray,但增加了在 GPU 上进行高效计算的能力。张量支持多种数据类型,如
float32
、int64
等。 - 自动求导(Autograd):PyTorch 的自动求导机制允许对张量的操作构建动态计算图,并自动计算梯度。这对于深度学习中的反向传播至关重要。
1.2 设备管理与 CUDA 张量
-
设备对象:PyTorch 使用
torch.device
对象来表示计算设备,如 CPU('cpu'
)和 GPU('cuda'
)。device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
-
CUDA 张量:当张量被分配到 GPU 设备时,它们被称为 CUDA 张量,可以利用 GPU 进行加速计算。
-
设备间操作:PyTorch 要求参与运算的所有张量必须在同一设备上,否则会引发错误。
1.3 CUDA 内核的调用过程
- 底层实现:PyTorch 的许多底层操作(如矩阵乘法、卷积等)都是使用 CUDA C/C++ 实现的,或者调用了 NVIDIA 提供的高度优化的库(如 cuBLAS、cuDNN)。
- CUDA 内核调用:
- 动态调用:PyTorch 在运行时根据需要调用相应的 CUDA 内核,利用 GPU 的并行计算能力。
- 自定义内核:高级用户可以编写自定义的 CUDA 扩展,以满足特殊的计算需求。
1.4 内存管理与数据传输
-
显式控制:用户需要显式地将数据和模型移动到 GPU 设备,这通过调用
.to(device)
方法实现。tensor = tensor.to(device) model.to(device)
-
内存分配:PyTorch 使用 CUDA 的内存管理 API,如
cudaMalloc
和cudaFree
,在 GPU 上分配和释放内存。 -
数据传输:
- 主机到设备(H2D):当调用
.to('cuda')
时,数据从 CPU 内存复制到 GPU 内存。 - 设备到主机(D2H):调用
.to('cpu')
或.cpu()
方法,将数据从 GPU 内存复制回 CPU 内存。
- 主机到设备(H2D):当调用
-
异步操作:PyTorch 中的 CUDA 操作默认是异步的,可以通过
torch.cuda.synchronize()
进行同步。
1.5 自动优化与执行
- 动态图机制:PyTorch 使用动态计算图,计算图在运行时构建,使得调试和开发更加灵活。
- 优化器:PyTorch 提供了多种优化器(如 SGD、Adam),用于更新模型参数。
- 后端优化:PyTorch 的后端会对计算进行优化,如操作融合,以提高性能。
2. MNIST 手写数字识别示例
2.1 数据加载与预处理
import torch
from torchvision import datasets, transforms# 定义设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')# 数据预处理
transform = transforms.Compose([transforms.ToTensor(), # 将图像转换为张量,并归一化到 [0,1]transforms.Normalize((0.1307,), (0.3081,)) # 标准化
])# 加载训练和测试数据集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform)# 定义数据加载器
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=1000, shuffle=False)
2.2 数据转换过程详解
- ToTensor():
- 作用:将 PIL Image 或 numpy.ndarray 转换为形状为
(C, H, W)
的张量(Tensor),并将像素值从[0, 255]
归一化到[0.0, 1.0]
。
- 作用:将 PIL Image 或 numpy.ndarray 转换为形状为
- Normalize(mean, std):
- 作用:使用给定的均值和标准差对张量进行标准化,即执行
(tensor - mean) / std
。
- 作用:使用给定的均值和标准差对张量进行标准化,即执行
- 数据加载器(DataLoader):
- 作用:提供批量化的数据迭代器,支持多线程数据加载,提高数据读取效率。
- 数据转换流程:
- 读取数据:从磁盘读取图像文件。
- 图像转换:将图像转换为张量,形状为
(1, 28, 28)
。 - 数据标准化:对张量进行标准化处理。
- 批次生成:将多个样本打包成批次。
2.3 模型定义与训练
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F# 定义卷积神经网络模型
class Net(nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1)self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1)self.dropout1 = nn.Dropout(0.25) # Dropout 层self.dropout2 = nn.Dropout(0.5)self.fc1 = nn.Linear(9216, 128) # 全连接层self.fc2 = nn.Linear(128, 10)def forward(self, x):x = self.conv1(x) # 卷积层1x = F.relu(x) # 激活函数x = self.conv2(x) # 卷积层2x = F.relu(x)x = F.max_pool2d(x, 2) # 池化层x = self.dropout1(x) # Dropoutx = torch.flatten(x, 1) # 展平x = self.fc1(x) # 全连接层1x = F.relu(x)x = self.dropout2(x) # Dropoutx = self.fc2(x) # 全连接层2output = F.log_softmax(x, dim=1) # 输出层return output# 实例化模型并移动到设备
model = Net().to(device)# 定义优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)# 训练函数
def train(model, device, train_loader, optimizer, epoch):model.train() # 设置为训练模式for batch_idx, (data, target) in enumerate(train_loader):# 将数据和标签移动到设备data, target = data.to(device), target.to(device)optimizer.zero_grad() # 梯度清零output = model(data) # 前向传播loss = F.nll_loss(output, target) # 计算损失loss.backward() # 反向传播optimizer.step() # 参数更新if batch_idx % 100 == 0:print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}'f' ({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')# 开始训练
for epoch in range(1, 6):train(model, device, train_loader, optimizer, epoch)
2.4 训练过程中的数据流动
- 数据加载到内存:
- 磁盘读取:
DataLoader
使用多线程从磁盘读取图像文件到主机内存(RAM)。 - 数据预处理:应用
ToTensor()
和Normalize()
对图像数据进行转换,得到标准化的张量。
- 磁盘读取:
- 数据传输到 GPU:
- 移动到设备:
data.to(device)
和target.to(device)
将数据从主机内存复制到 GPU 显存。 - 底层实现:调用 CUDA 的
cudaMemcpy
函数完成数据传输。
- 移动到设备:
- 前向传播(在 GPU 上执行):
- 卷积层计算:
nn.Conv2d
调用 cuDNN 库,在 GPU 上高效执行卷积操作。 - 激活函数:
F.relu
在 GPU 上执行非线性激活。 - 池化层:
F.max_pool2d
在 GPU 上执行最大池化操作。 - Dropout:
nn.Dropout
在训练时随机丢弃部分神经元,防止过拟合。 - 展平和全连接层:
torch.flatten
和nn.Linear
在 GPU 上执行矩阵运算。 - Softmax 输出:
F.log_softmax
计算每个类别的对数概率。
- 卷积层计算:
- 反向传播(在 GPU 上执行):
- 损失计算:
F.nll_loss
计算负对数似然损失。 - 梯度计算:
loss.backward()
触发自动求导机制,计算损失函数对模型参数的梯度。
- 损失计算:
- 参数更新:
- 优化器更新:
optimizer.step()
使用计算得到的梯度更新模型参数。 - CUDA 加速:参数更新操作也在 GPU 上执行,充分利用并行计算能力。
- 优化器更新:
- 同步与异步:
- 异步执行:默认情况下,CUDA 操作是异步的,可以加速计算。
- 必要时同步:在需要获取准确的时间测量或确保计算完成时,可以调用
torch.cuda.synchronize()
。
总结:
PyTorch 通过对张量和自动求导机制的封装,以及对设备的显式管理,实现了对 CUDA 的高效调用。用户需要明确地将数据和模型移动到 GPU,这使得数据在设备之间的流动更加透明。在 MNIST 手写数字识别的示例中,我们详细阐述了数据在模型训练过程中的转换和流动过程。数据从磁盘读取后,经过预处理和转换,形成标准化的张量。然后,数据和模型被移动到 GPU 内存。在 GPU 上,利用 CUDA 内核和 NVIDIA 提供的高度优化的库(如 cuDNN、cuBLAS),完成前向传播和反向传播的计算。整个过程中,PyTorch 的自动求导机制和动态图特性使得模型的开发和调试更加方便,同时充分利用了 GPU 的计算能力。
https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
https://pytorch.org/docs/stable/notes/cuda.html
https://pytorch.org/docs/stable/notes/autograd.html
https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html
https://github.com/pytorch/examples/tree/main/mnist
https://pytorch.org/blog/a-tour-of-pytorch-internals-1/
3. MXNet内部调用cuda
1.MXNet 底层调用 CUDA 的理论基础
11. NDArray 与数据表示
- NDArray:MXNet 中的数据基本单位,支持多维数组操作。
- 内存管理:NDArray 可以分配在 CPU 或 GPU 内存中,内部封装了指向实际数据的指针。
- 异步执行:操作是异步的,通过引擎(Engine)调度执行。
1.2. 操作符(Operator)机制
- Operator 定义:每个算子都有对应的前向和反向实现,支持 CPU 和 GPU 版本。
- 注册机制:使用
RegisterOp
宏,将算子注册到 MXNet 系统中。 - GPU 实现:GPU 版本的算子实现通常使用 CUDA 或 cuDNN。
1.3. 引擎(Engine)与任务调度
- 引擎作用:负责管理计算任务的依赖关系和调度执行。
- 任务队列:操作被包装成任务,放入引擎的任务队列中。
- 异步执行:引擎利用多线程和 CUDA 流,实现异步并行执行。
1.4. CUDA 调用流程
- CUDA 上下文:MXNet 初始化时,会创建 CUDA 上下文,管理 GPU 资源。
- CUDA 内核调用:在 GPU 版本的算子中,使用 CUDA C++ 编写内核函数。
- cuDNN 集成:对于复杂的神经网络操作,MXNet 调用 cuDNN 提供的高性能实现。
1.5. 内存与数据转换
- 主机与设备内存:数据需要在 CPU(主机)和 GPU(设备)内存之间传输。
- 数据同步:MXNet 提供同步和异步的 API,控制数据的读写和一致性。
- 内存池:采用内存池机制,减少频繁的内存分配和释放。
2.MINIST手写数字识别
2.1. 数据加载与预处理
import mxnet as mx
from mxnet import nd, gluon, autograd
from mxnet.gluon import nn, data as gdata# 定义上下文,使用 GPU
ctx = mx.gpu() if mx.context.num_gpus() > 0 else mx.cpu()# 加载 MNIST 数据集
mnist_train = gdata.vision.MNIST(train=True)
mnist_test = gdata.vision.MNIST(train=False)# 定义数据迭代器
batch_size = 256
train_iter = gdata.DataLoader(mnist_train.transform_first(lambda data, label: (data.astype('float32')/255, label)),batch_size, shuffle=True)
test_iter = gdata.DataLoader(mnist_test.transform_first(lambda data, label: (data.astype('float32')/255, label)),batch_size, shuffle=False)
- 数据加载:从磁盘读取数据,数据初始位于主机内存。
- 数据转换:将图像数据转换为
float32
类型,归一化处理。 - 数据传输:在训练过程中,数据将被复制到 GPU 内存中。
2.2. 模型定义
# 定义简单的多层感知机
net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'),nn.Dense(10))
net.initialize(ctx=ctx)
- 参数初始化:模型参数会根据上下文
ctx
分配到 CPU 或 GPU 内存。 - GPU 内存分配:如果使用 GPU,上述参数会被分配到设备内存。
3. 前向计算与 CUDA 调用
for data, label in train_iter:data = data.as_in_context(ctx).reshape((-1, 784))label = label.as_in_context(ctx)with autograd.record():output = net(data)loss = gluon.loss.SoftmaxCrossEntropyLoss()(output, label)loss.backward()trainer.step(batch_size)
- 数据传输:
data.as_in_context(ctx)
将数据复制到 GPU 内存。 - 前向计算
- NDArray 操作:
net(data)
触发一系列 NDArray 的计算操作。 - CUDA 内核调用:对应的算子在 GPU 上执行,调用 CUDA 内核。
- NDArray 操作:
- 反向传播
- 自动求导:
autograd.record()
记录计算图。 - CUDA 调用:反向算子同样在 GPU 上执行。
- 自动求导:
2.4. 操作细节与 CUDA 集成
- 矩阵乘法:
Dense
层的计算涉及矩阵乘法,MXNet 调用 cuBLAS 库实现。 - 激活函数:
ReLU
等激活函数,可能使用自定义的 CUDA 内核。 - 损失函数:
SoftmaxCrossEntropyLoss
可能调用 cuDNN 的高性能实现。
2.5. 引擎调度与异步执行
-
任务创建:每个 NDArray 操作被包装成任务,提交给引擎。
-
依赖管理:引擎管理操作之间的依赖,确保正确的执行顺序。
-
CUDA 流:利用 CUDA 流,实现异步的 GPU 计算,与 CPU 计算重叠。
-
NDArray 实现:
src/ndarray/ndarray.cc
-
Operator 注册:
src/operator/
目录下,各种算子的实现和注册。 -
引擎实现:
src/engine/
目录,包含任务调度和引擎机制。
参考资料
-
MXNet NDArray:https://mxnet.apache.org/versions/master/api/ndarray/index.html
-
使用 GPU:https://mxnet.apache.org/versions/master/faq/gpu_support.html
-
自动求导:https://mxnet.apache.org/versions/master/api/autograd/index.html
-
GitHub 仓库:https://github.com/apache/mxnet
-
CUDA 开发者指南:https://docs.nvidia.com/cuda/
-
cuDNN 开发者指南:https://docs.nvidia.com/deeplearning/cudnn/developer-guide/index.html
-
MXNet 论文:“MXNet: A Flexible and Efficient Machine Learning Library for Heterogeneous Distributed Systems”