当前位置: 首页 > article >正文

cuda与机器学习

cuda与机器学习框架

1.基础概述

CUDA是由NVIDIA开发的并行计算平台和编程模型,允许开发者利用支持CUDA的NVIDIA GPU来加速计算密集型任务。CUDA提供了扩展的C/C++语言,以及用于在GPU上执行并行计算的API。

线程、线程块、网格、束

  1. 线程

    • 基本执行单元:在 GPU 中,线程是最小的执行单元,负责执行 CUDA 内核函数中的指令。

    特点

    • 轻量级:GPU 线程相比 CPU 线程更加轻量级,创建和切换开销小。
    • 大量存在:一次可以有成千上万的线程同时存在,发挥 GPU 的并行计算能力。
    • 独立执行:每个线程都有自己的寄存器和本地内存,可以独立执行。
    • 线程索引:每个线程可以通过内置变量获取自己的线程索引,如 threadIdx.x
  2. 线程块

  • 线程的集合:线程块是多个线程的集合,组成一个可在 GPU 上执行的基本调度单元。

    特点

    • 共享内存:同一线程块内的线程可以共享快速的片上共享内存,支持线程间的数据交换。
    • 同步机制:线程块内的线程可以使用 __syncthreads() 等同步指令进行同步。
    • 最大尺寸:线程块的大小受 GPU 架构的限制,通常是 1D、2D 或 3D 的维度。
  • 线程块索引:通过 blockIdx.xblockIdx.yblockIdx.z 获取线程块的索引。

  • 线程块大小:通过 blockDim.xblockDim.yblockDim.z 获取线程块的维度大小。

  • 全局线程索引:结合线程块索引和线程索引,可以计算线程在整个网格中的全局索引。

    int global_thread_id = blockIdx.x * blockDim.x + threadIdx.x;
    
  1. 网格
  • 线程块的集合:网格是线程块的集合,用于组织和管理 GPU 上的大规模并行计算。

特点

  • 独立执行:不同的线程块可以独立执行,之间不能直接通信。
  • 尺寸灵活:网格可以是一维、二维或三维,方便映射复杂的数据结构。
  • 网格大小:通过 gridDim.xgridDim.ygridDim.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程序的编译和链接过程:

  1. NVCC编译器:
  • CUDA程序通常使用nvcc(NVIDIA CUDA Compiler)编译器进行编译。
  • nvcc是一个编译器驱动程序,它调用其他编译器(如gcccl.exe)来编译主机代码,同时使用NVIDIA自己的工具链来编译设备代码(GPU上的代码)。
  1. 代码分离:

CUDA程序包含两种代码:

  • 主机代码(Host Code): 在CPU上运行的代码,使用标准的C/C++语法。
  • 设备代码(Device Code): 在GPU上运行的代码,使用CUDA扩展的C/C++语法,包含核函数定义。
  1. 编译过程:
  1. 预处理: 处理``#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端的工作:
    1. 初始化:
      • 配置CUDA环境,选择GPU设备。
      • 分配主机内存,初始化数据。
    2. 内存管理:
      • 使用cudaMalloc()在GPU上分配设备内存。
      • 使用cudaMemcpy()将数据从主机复制到设备。
    3. 核函数启动:
      • 使用核函数调用语法启动GPU上的并行计算。
      • CPU线程将执行命令发送到GPU,但不等待GPU完成。
    4. 继续执行或同步:
      • CPU可以继续执行后续代码。
      • 如果需要结果,CPU会调用cudaDeviceSynchronize()等待GPU完成计算。
    5. 数据传回与清理:
      • 使用cudaMemcpy()从设备复制结果回主机。
      • 释放设备内存,清理资源。
  • GPU端的工作:
    1. 接受命令:
      • GPU接收来自CPU的核函数执行命令。
    2. 线程调度与执行:
      • 根据配置,GPU创建线程网格和线程块。
      • 每个线程执行核函数代码,处理数据。
    3. 内存访问:
      • 线程访问设备内存,包括全局内存、共享内存、常量内存等。
    4. 同步与通信:
      • 线程块内的线程可以使用__syncthreads()进行同步。
      • 不同线程块之间通过全局内存通信。
    5. 完成计算:
      • 核函数执行结束,GPU通知驱动程序。

4. CPU和GPU之间的异步与同步:

  • 异步执行:
    • 核函数调用和数据传输可以是异步的,CPU无需等待操作完成。
    • 这允许CPU和GPU并行工作,提高性能。
  • 同步操作:
    • 使用cudaDeviceSynchronize()cudaStreamSynchronize()等函数使CPU等待GPU完成。
    • 使用CUDA事件(Event)进行精细的同步控制。

5.内存管理与数据传输:

  1. 设备内存分配:
  • cudaMalloc(void** devPtr, size_t size):在GPU上分配内存。
  • cudaFree(void* devPtr):释放GPU内存。
  1. 数据传输:
cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)
  • 将数据在主机和设备之间复制。
  • cudaMemcpyHostToDevicecudaMemcpyDeviceToHostcudaMemcpyDeviceToDevice等。
  1. 统一内存(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:
      • 执行并行计算,加速数据处理。
  • 内存管理:
    • 主机和设备各自管理自己的内存空间。
    • 使用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设备。


主要特点:

  1. 多平台支持:TensorFlow可以在CPU、GPU和TPU上运行,支持从桌面设备到服务器集群以及移动设备的部署。
  2. 易于使用的API:提供了高级的Keras接口,使模型构建更加简洁明了,同时也支持低级API以满足高级用户的需求。
  3. 灵活的架构:采用符号式和命令式编程相结合的方式,既支持静态计算图又支持动态图,使开发者能够根据需求选择合适的编程模型。
  4. 丰富的生态系统
    • TensorFlow Hub:共享和复用机器学习模型的平台。
    • TensorFlow Lite:用于移动和嵌入式设备的轻量级解决方案。
    • TensorFlow Extended (TFX):用于生产环境的端到端平台,涵盖数据验证、模型训练、服务等。
  5. 强大的社区支持:拥有全球活跃的开发者社区,丰富的教程、示例和第三方库。

应用领域:

  • 计算机视觉:图像分类、目标检测、图像分割等。
  • 自然语言处理:文本分类、机器翻译、文本生成等。
  • 语音识别与合成:语音到文本转换、语音情感分析等。
  • 强化学习:游戏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等库,以加速深度学习计算。


主要特点:

  1. 动态计算图(Dynamic Computational Graph):
  • PyTorch采用了动态计算图机制,允许在运行时改变网络结构。这使得模型的构建和调试更加灵活,特别适合于需要动态变化的神经网络,如循环神经网络(RNN)和自然语言处理(NLP)任务。
  1. Pythonic风格:
    • PyTorch的API设计非常贴近Python的编程习惯,代码简洁直观,易于上手。这降低了学习曲线,使开发者能够专注于模型本身。
  2. 强大的社区和生态系统:
    • 拥有活跃的开源社区,提供了丰富的第三方库和工具,如torchvision(计算机视觉)、torchtext(自然语言处理)和torchaudio(音频处理)等。
  3. 自动微分机制:
    • 内置了Autograd模块,可以自动计算张量的梯度,方便实现反向传播,简化了深度学习模型的训练过程。
  4. 多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。


主要特点:

  1. 灵活的编程模型:
    • 混合式编程(Hybrid Programming): MXNet支持命令式和符号式编程的结合。通过Gluon接口,开发者可以方便地定义、训练和部署模型,既享受命令式编程的灵活性,又具备符号式编程的高性能。
  2. 高性能和可扩展性:
    • 高效计算: 针对GPU和CPU进行了高度优化,支持自动并行计算,加速模型训练和推理。
    • 分布式训练: 原生支持多机多卡的分布式训练,能够处理大型数据集和复杂模型。
  3. 丰富的生态系统:
    • Gluon接口: 一个简洁而强大的高级API,使得模型构建、训练和部署更加直观。
    • 预训练模型库: 提供大量预训练模型,方便进行迁移学习和快速原型开发。
  4. 跨平台支持:
    • 多语言支持: 除了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进行了高度优化,提供了常用操作的高效实现,如卷积、池化和矩阵乘法等。

  1. 并行计算: 通过CUDA,框架可以在GPU上并行执行大量计算。GPU擅长处理大规模的并行任务,例如对大型张量进行元素级操作。这使得深度学习模型的训练和推理速度显著提升。
  2. 自定义CUDA内核: 对于高级用户,某些框架允许编写自定义的CUDA内核,以优化特定的操作。例如,在TensorFlow中,可以使用CUDA代码来编写自定义的OP(操作符),以实现特殊的功能或优化性能。
  3. 设备抽象: 为了提高可移植性,这些框架通常提供了设备抽象层。开发者可以编写与设备无关的代码,框架会根据实际运行环境,将计算分配到CPU或GPU上。这使得代码能够在不同的硬件配置上运行,而无需修改。
  4. 优化与调优: 框架与CUDA的集成允许利用NVIDIA的性能分析和调优工具,如nvprof和Nsight。这些工具可以帮助开发者分析GPU的性能瓶颈,进行优化。
  5. 内存管理: 框架通常负责管理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:foo
      with tf.device('/job:bar/task:0/device:gpu:2'):
        # ops created here have the fully specified device above
      with 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 内核。
  • 调用流程
    1. 准备数据:将需要计算的数据准备好,确保数据在 GPU 内存中。
    2. 设置参数:配置 CUDA 内核的参数,包括线程块大小、网格大小等。
    3. 启动内核:通过 CUDA 的 API,启动内核函数在 GPU 上执行。
    4. 同步与异步执行:TensorFlow 利用 CUDA 的流(Stream)和事件(Event)机制,支持异步计算和操作间的依赖。

1.4 内存管理与数据传输

  • 内存分配:TensorFlow 使用 CUDA 的内存管理 API,如 cudaMalloccudaFree,在 GPU 上分配和释放内存。
  • 数据传输
    • 主机到设备:当数据从 CPU 传输到 GPU 时,使用 cudaMemcpy 等函数。
    • 设备到设备:在多个 GPU 之间传输数据,使用 cudaMemcpyPeer 等函数。
    • 异步传输:为了提高效率,数据传输可以与计算并行,利用 CUDA 的异步拷贝功能。

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 的运行时会优化内存使用,复用内存块,减少内存分配和释放的次数。
  • 多线程与异步执行

    • 数据预处理线程:数据加载和预处理可以在 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 上进行高效计算的能力。张量支持多种数据类型,如 float32int64 等。
  • 自动求导(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,如 cudaMalloccudaFree,在 GPU 上分配和释放内存。

  • 数据传输

    • 主机到设备(H2D):当调用 .to('cuda') 时,数据从 CPU 内存复制到 GPU 内存。
    • 设备到主机(D2H):调用 .to('cpu').cpu() 方法,将数据从 GPU 内存复制回 CPU 内存。
  • 异步操作: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]
  • Normalize(mean, std)
    • 作用:使用给定的均值和标准差对张量进行标准化,即执行 (tensor - mean) / std
  • 数据加载器(DataLoader)
    • 作用:提供批量化的数据迭代器,支持多线程数据加载,提高数据读取效率。
  • 数据转换流程
    1. 读取数据:从磁盘读取图像文件。
    2. 图像转换:将图像转换为张量,形状为 (1, 28, 28)
    3. 数据标准化:对张量进行标准化处理。
    4. 批次生成:将多个样本打包成批次。

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)    # 卷积层1
        x = F.relu(x)        # 激活函数
        x = self.conv2(x)    # 卷积层2
        x = F.relu(x)
        x = F.max_pool2d(x, 2)  # 池化层
        x = self.dropout1(x)    # Dropout
        x = torch.flatten(x, 1) # 展平
        x = self.fc1(x)         # 全连接层1
        x = F.relu(x)
        x = self.dropout2(x)    # Dropout
        x = self.fc2(x)         # 全连接层2
        output = 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 上执行最大池化操作。
    • Dropoutnn.Dropout 在训练时随机丢弃部分神经元,防止过拟合。
    • 展平和全连接层torch.flattennn.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 内核。
  • 反向传播
    • 自动求导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”


http://www.kler.cn/news/311732.html

相关文章:

  • C++ ——string的模拟实现
  • 字节跳动的微服务独家面经
  • 详细分析Pytorch中的register_buffer基本知识(附Demo)
  • 9.19工作笔记
  • fmql之驱动程序编写(首次)
  • 浏览器插件利器--allWebPluginV2.0.0.20-beta版发布
  • 安科瑞智能塑壳断路器适用于物联网配电电网中-安科瑞黄安南
  • 算法打卡:第十一章 图论part01
  • 每天五分钟深度学习框架pytorch:pytorch中已经定义好的损失函数
  • Redis的Key的过期策略是怎样实现的?
  • 【WPF】02 按钮控件圆角配置及状态切换
  • 华为昇腾智算中心-智算中心测试方案与标准
  • JavaEE:网络编程(UDP)
  • pg入门3—详解tablespaces2
  • Elasticsearch 8.+ 版本查询方式
  • JVM 内存结构?
  • 卖家必看:利用亚马逊自养号测评精选热门产品,增强店铺权重
  • 运维工程师面试整理-监控与报警监控系统
  • uni-app-通过vue-cli命令行快速上手
  • 数据结构(Day16)
  • 虎先锋,你也喜欢线程控制嘛
  • UAC2.0 麦克风——音量控制
  • etcd之etcd简介和安装(一)
  • 全面整理的Oracel 数据库性能调优方案
  • 关系运算符
  • vue选项式写法项目案例(购物车)
  • 制作网上3D展馆需要什么技术并投入多少费用?
  • JSP分页功能实现案例:从基础到应用的全面解析
  • python SQLAlchemy 数据库连接池
  • 《拿下奇怪的前端报错》序章:报错输出个数值数组Buffer(475) [Uint8Array],我来教它说人话!