CUDA架构与高性能程序设计:多维网格与数据

引言

在前面的章节中,我们已经揭开了 GPU 大规模并行计算的神秘面纱,并成功编写了基础的 CUDA 程序。利用一维的线程网格(Grid)和线程块(Block),我们学会了如何高效地处理线性的数据数组,例如向量加法。然而,当我们把目光从简单的数组投向广阔的现实世界时,会发现一个不可回避的事实:真实世界的计算问题绝大多数都是多维的。

无论是计算机视觉中由像素矩阵构成的二维高清图像,还是流体力学模拟中复杂的三维空间网格,亦或是深度学习神经网络中庞大的多维张量(Tensor),数据的逻辑结构往往呈现出丰富的空间维度。

如果我们强行用一维的思维去处理这些多维数据,程序员就必须在代码中手动进行繁琐且极耗算力的坐标换算。这不仅会让代码变得晦涩难懂,更容易引入隐蔽的越界错误。更重要的是,它违背了 GPU 编程的核心哲学------让硬件的执行模式尽可能地贴合数据的自然形态。

本章将带领大家实现从"一维线性思维"到"多维空间思维"的跨越。我们将深入探讨物理内存的底层限制,并学习 CUDA 是如何通过优雅的内置变量(如 dim3 结构)原生支持多维线程组织的。

在本章中,你将学习到:

  • 内存映射原理: 理解多维逻辑数据是如何被"展平"并存储在一维的物理内存(RAM/VRAM)中的。

  • 多维坐标推导: 掌握如何利用 blockIdxthreadIdx 等内置变量,计算出线程在全局多维网格中的绝对坐标。

  • 图像处理实战: 通过编写经典的"彩色转灰度"核函数,将多维线程直接映射到二维图像的像素点上。

  • 边界保护机制: 学习在网格尺寸与数据尺寸不匹配时,如何编写健壮的边界检查代码以防止内存越界。

  • 矩阵运算初探: 实现基础的矩阵乘法(Matrix Multiplication),理解多维数据在复杂计算中的内存访问模式,并为后续的性能优化章节埋下伏笔。

掌握多维网格与数据的映射,是编写所有高级 GPU 算法的必经之路。让我们开始这场多维空间的探索之旅吧。

第一章:动机与应用背景

在深入探讨 CUDA 的多维网格和线程块之前,我们必须先理解一个计算机科学中基础但至关重要的矛盾:物理内存的一维性与现实数据的多维性之间的冲突。

1.物理内存的一维本质

计算机的物理内存本质上是线性的,就像一条长长的一维街道。但是,我们在做并行计算时,处理的数据往往是二维的,比如矩阵或图像的像素。

为了把二维的数据存进一维的内存,CUDA(和 C/C++ 一样)采用了行主序 (Row-major order) 的方式。也就是说,系统会先完整地存放第 0 行的所有元素,接着存放第 1 行,依此类推。

你可以看这个 3x3 矩阵的例子:

列 0 列 1 列 2
行 0 A B C
行 1 D E F
行 2 G H I

在物理内存(一维数组)中,它会被"展平"并按顺序存放为:

[A, B, C, D, E, F, G, H, I]

为了让数以万计的 CUDA 线程准确找到自己负责的数据,我们需要建立二维坐标 到一维数组索引 之间的数学映射。

很自然我们可以得出:

注意: 在编写大规模并行程序时,必须清楚地知道底层数据采用的是哪种主序。如果在一个假设数据是行主序的 CUDA C++ 程序中,错误地按列主序去读取数据,不仅会得到错误的结果,还会引发严重的内存访问性能问题(我们将在此后的"内存合并访问"一章中详细讨论)。

2 多维数据的线性映射 (Row-Major Layout)

那么,如果我们有一张 像素的二维灰度图像,它是如何塞进这一维的内存带子里的呢?

这就需要一种"展平(Flattening)"策略。在 C/C++ 以及 CUDA 中,最标准的做法是行主序(Row-Major Order)

具体来说,计算机会先将图像的第 行依次存入内存,接着紧挨着存入第 行,然后是第 行......以此类推。

假设图像的宽度为 ,对于图像中任意一个逻辑坐标为 的像素(其中X为列号, 为行号),它在内存中的一维线性索引 的计算公式为:

这个公式是所有多维数据处理的基石。它意味着,为了跨越到下一行( 增加 ),我们在物理内存中需要跳过一整行的宽度( 个元素)。

如果我们坚持使用一维的线程结构去处理一张二维的图像,会发生什么?

假设我们有一张宽度为 的二维图像。如果我们启动一个一维的线程块,每个线程有一个唯一的线性索引 。为了让这个线程知道自己应该处理图像中的哪个像素(即行号 和列号 ),程序员必须在核函数(Kernel)中手动进行坐标换算:

  • 计算行号:

  • 计算列号:

这种做法有两个显著的缺点:

  1. 代码缺乏直观性: 程序员需要不断地在"一维线性空间"和"二维数据空间"之间进行大脑体操,这极易导致索引越界或错位等Bug。

  2. 性能损耗: 在GPU底层硬件中,整数的除法(/)和取模(%)运算是非常昂贵的指令,它们消耗的时钟周期远多于加法和乘法。让成千上万个线程在每次访问数据前都执行这些昂贵的指令,是对计算资源的巨大浪费。

3. 破局:多维线程组织

为了解决上述问题,CUDA编程模型优雅地引入了多维网格和线程块的概念。它允许我们直接启动一个二维的线程块去覆盖二维的图像数据。

在多维模型中,线程自身就带有二维坐标属性(例如 threadIdx.xthreadIdx.y)。这意味着,线程 可以直接、天然地去处理图像中坐标为 的像素。除法和取模运算被彻底消除了,代码逻辑变得与数据的物理形态完全一致。

在 CUDA 和大多数图形处理场景中,x 通常对应的是列 (Col)

我们可以回忆一下数学里的平面直角坐标系:

  • x 轴是水平方向的(左右移动),这正好对应矩阵中列 (Col) 的变化。

  • y 轴是垂直方向的(上下移动),对应矩阵中行 (Row) 的变化。

所以,习惯上我们会建立这样的映射关系:

  • Col = threadIdx.x

  • Row = threadIdx.y

把它们代入我们刚才推导的展平公式,在只有单个二维线程块 (Block) 的情况下,每个线程计算一维内存索引的公式就变成了:

假设矩阵的宽度为 ,对于图像中任意一个逻辑坐标为 的元素(其中 为列号, 为行号),它在内存中的一维线性索引 的计算公式为:

这个公式是所有多维数据处理的基石。它意味着,为了跨越到下一行( 增加 ),我们在物理内存中需要跳过一整行的宽度(个元素)。

第二章:多维线程组织结构

在理解了物理内存布局与多维数据的映射关系后,我们来看看 CUDA 是如何提供原生的多维线程支持的。CUDA 允许程序员以一维、二维或三维的方式来组织网格(Grid)和线程块(Block)。

1.dim3 向量类型

为了定义多维的执行配置,CUDA 提供了一个专门的内置结构体:dim3。它是一个基于无符号整数(unsigned int)的三维向量类型,包含三个成员变量:xyz

当你定义一个 dim3 变量时,任何未被显式指定的维度都会被默认初始化为 1

cpp 复制代码
// 定义一个 16 x 16 的二维线程块 (z 默认为 1)
dim3 blockDim(16, 16); 

// 定义一个包含 4 x 2 个线程块的二维网格 (z 默认为 1)
dim3 gridDim(4, 2);

2 CUDA 内置的坐标与维度变量

回顾一下上一章我们讲的:

在 CUDA 编程模型中,执行同一段内核程序(Kernel)的所有线程构成一个网格(Grid) 。由于要处理的数据量通常极其庞大(例如数百万个像素),且硬件资源有限,网格内部被进一步划分为多个大小相同的线程块(Thread Block) 。每个线程块内部又包含多个线程(Thread)

因此,GPU 的线程模型呈现出经典的两级层次结构

  1. Grid(网格): 由多个 Block 组成。

  2. Block(线程块): 由多个 Thread 组成。

当核函数(Kernel)启动后,GPU 硬件会自动为每个线程分配一组内置变量,这些变量就像是线程的"GPS 定位系统",让每个线程都知道自己处于庞大网格中的哪个位置。对于二维组织,我们需要重点关注以下四个内置变量(它们在底层类似于 uint3 类型):

  • gridDim(网格维度): 网格中每个维度上包含的线程块数量 。例如,gridDim.xgridDim.y

  • blockDim(线程块维度): 每个线程块中包含的线程数量 。例如,blockDim.xblockDim.y

  • blockIdx(线程块索引): 当前线程所在的线程块在网格中的坐标。索引从 0 开始。

  • threadIdx(线程索引): 当前线程在它所属的线程块内部的局部坐标。索引从 0 开始。

为什么不直接用一个巨大的 Block 包含所有线程?这是因为现代 GPU 的硬件架构(流多处理器,SM)对单个 Block 的资源有严格限制。例如,在大多数现代 CUDA 架构中,一个 Block 最多只能包含 1024 个线程。因此,对于规模稍大的多维数据,我们必须将其拆分到多个 Block 中,形成 Grid。

3 核心公式:多维坐标到全局一维索引的映射

这是并行程序设计中最关键的一环。当我们将多维的 Grid 映射到多维的数据(例如一张二维图像)时,我们需要计算出当前线程在整个大网格中的全局唯一坐标,然后再利用 3.1 节学过的内存展平公式,找到其在物理内存中的地址。

接下来,我们需要把视角放大。当处理像 1080p 图片这样包含上百万像素的数据时,一个 Block(硬件限制最多 1024 个线程)是远远不够用的。我们需要用许多个 Block 拼成一个多维网格 (Grid)。

为了在整个网格中准确定位全局的 ColRow,我们需要引入另外两个内置变量:

  1. blockDim: 一个 Block 的尺寸(例如 blockDim.x 代表一个 Block 包含多少列)。

  2. blockIdx: 当前 Block 在整个 Grid 中的坐标(从 0 开始)。

我们来算一步:假设我们把图片划分成了多个 大小的 Block(即 blockDim.x = 16)。如果一个线程所在的 Block 的横向索引是 2(即 blockIdx.x = 2),并且该线程在自己 Block 内部的横向索引是 5(即 threadIdx.x = 5)。

你能试着推导出这个线程在整张大图片中真实的全局列号 (Global Col) 吗?

我们可以把处理大图片的整个网格(Grid)想象成一列长长的火车,而线程块(Block)就是火车上的一节节车厢

我们把刚才的数据套用到这个比喻里:

  • blockDim.x = 16:代表每节车厢里一排有 16 个座位

  • blockIdx.x = 2:代表你所在的线程位于第 2 节车厢(注意,车厢编号是从 0 开始的,也就是你前面还有第 0 节和第 1 节车厢,共计 2 节完整的车厢)。

  • threadIdx.x = 5:代表你在自己这节(第 2 节)车厢里,坐在第 5 个座位

要想知道你是整列火车的第几个座位(也就是我们要求的全局列号 Global Col),我们可以分两步来算。

第一步,我们先算出排在你前面的那些完整车厢里,一共有多少个座位

很显然,2x16 = 32个座位。

这 32 个座位,就代表了在你前面的那些线程块(Block)里一共包含了 32 个线程。在 CUDA 代码里,这个计算过程就是 blockIdx.x * blockDim.x

现在我们进行最后一步:既然你前面的车厢已经占了 32 个座位,而你在当前这节车厢里坐在第 5 个座位(也就是 threadIdx.x = 5)。

把前面的 32 加上你当前的相对位置 5,就是整列火车(整个 Grid)里的全局编号

把我们刚才算的两步合并起来,在 CUDA 程序中计算全局列坐标的标准公式就是:

同理

这样我们就得到了全局的COL和ROW坐标,然后根据之前的公式

现在,我们把这两部分结合起来。把公式里的 替换成我们算出的 ,把 替换成

Width 指的是我们要处理的整个二维数据(比如整张图片或整个大矩阵)的总宽度

换句话说,它代表了实际数据中完整的一行包含多少个元素(也就是总列数)。

请看以下图示,假设我们的网格规模:

对于橙色的块我们可以得到:

通过公式计算:

可以得出全局ID:

第三章:实践

1.矩阵加法

现在,我们将这些理论结合起来,编写一个真实的 CUDA 内核(Kernel)函数来实现两个二维矩阵的加法运算:

假设矩阵 A,B 和 C 具有相同的尺寸,宽度为 W(列数),高度为 H(行数),并且在内存中均以行主序存储

在编写代码之前,我们需要先在脑海中建立一幅清晰的映射图: 想象矩阵 是一个由 个方格组成的巨大棋盘。我们将配置一个二维的线程网格(Grid)像一张大网一样覆盖在这个棋盘上。网格中的每一个交叉点(线程),就专门负责计算棋盘上的一个特定方格(即 )。

下面是执行矩阵加法的 CUDA C++ 内核代码。请注意观察我们在 上节推导的坐标公式是如何在代码中应用的。

cpp 复制代码
// CUDA 内核函数:执行二维矩阵加法
__global__ void matrixAddKernel(float *A, float *B, float *C, int width, int height) {
    // 步骤 1:利用内置变量,计算当前线程负责的全局二维坐标 (Col, Row)
    int Col = blockIdx.x * blockDim.x + threadIdx.x;
    int Row = blockIdx.y * blockDim.y + threadIdx.y;

    // 步骤 2:边界检查(防止内存越界访问)
    if (Col < width && Row < height) {
        // 步骤 3:利用映射公式,将二维坐标展平为一维物理内存索引
        int linearIndex = Row * width + Col;

        // 步骤 4:在一维内存地址上执行加法操作
        C[linearIndex] = A[linearIndex] + B[linearIndex];
    }
}
  • __global__ 修饰符: 这告诉编译器,该函数是在设备端(GPU)上执行的,并且可以从主机端(CPU)被调用。

  • 坐标计算: ColRow 的计算完全依赖于硬件提供的寄存器变量,执行速度极快。

  • 边界检查 if (Col < width && Row < height) 这是多维并行编程中极其关键的一步。由于线程块的大小(blockDim)通常是固定的(例如 ),而实际矩阵的宽度和高度往往不能被 16 完美整除。这会导致网格的边缘超出了矩阵的实际边界。如果不加这个 if 判断,边缘的线程就会尝试访问非法的内存地址,导致程序崩溃(Segmentation Fault)。

  • 一维索引 linearIndex 完美印证了我们推导的行主序展平公式。

写好了在 GPU 上运行的内核,我们还需要在 CPU 端(Host)正确地配置 dim3 结构并启动它。这是很多初学者容易卡壳的地方。

cpp 复制代码
// 假设 width 和 height 是矩阵的实际尺寸
int width = 1000;
int height = 800;

// 1. 定义线程块 (Block) 的维度
// 通常选择 16x16 或 32x32,这里一个 Block 包含 256 个线程
dim3 threadsPerBlock(16, 16);

// 2. 定义网格 (Grid) 的维度
// 核心技巧:使用向上取整除法,确保生成的线程总数足以覆盖整个矩阵
dim3 numBlocks(
    (width + threadsPerBlock.x - 1) / threadsPerBlock.x,
    (height + threadsPerBlock.y - 1) / threadsPerBlock.y
);

// 3. 启动内核
matrixAddKernel<<<numBlocks, threadsPerBlock>>>(d_A, d_B, d_C, width, height);

配置技巧:向上取整(Ceiling Division) 在计算 numBlocks 时,我们使用了 (N + M - 1) / M 这种 C++ 中经典的向上取整写法。 如果矩阵宽度 width = 1000,而 threadsPerBlock.x = 16,那么单纯的整数除法 。62 个 Block 只能覆盖 列,最后 8 列将无法被计算。 使用 (1000 + 16 - 1) / 16 = 63,我们将启动 63 个 Block,覆盖 列。多出来的 8 个线程会因为内核函数中的 if (Col < width) 边界检查而直接闲置,从而保证了计算的正确性。

2.RGB转灰度

为了将理论应用于实践,我们来编写一个 CUDA 核函数,将一张 RGB 彩色图像转换为灰度图像。

在代码中,每个线程都会先计算出自己的 ,然后去读取相应的像素。

但在真正读取数据之前,实际的工程代码中通常都必须加一个非常关键的 if 判断来进行边界检查 (Boundary Check)

在计算机视觉中,图像通常被存储为一维数组。

  • 输入数据 (RGB): 每一个像素包含红 (R)、绿 (G)、蓝 (B) 三个通道的值(通常为 0~255 的 unsigned char)。在内存中,它们以 R, G, B, R, G, B... 的顺序交错排列。因此,一张宽为 、高为 的彩色图像,其输入数组的总大小为

  • 输出数据 (灰度): 灰度图像每个像素只有一个亮度值。因此,输出数组的总大小仅为

逻辑像素编号 (Index) 0 1 2 ...
物理内存数组存放 [R, G, B] [R, G, B] [R, G, B] ...
真实数组下标 0, 1, 2 3, 4, 5 6, 7, 8 ...

我们来找找规律:

  • 第 0 个像素的 R 在下标 0 * 3 = 0

  • 第 1 个像素的 R 在下标 1 * 3 = 3

  • 第 2 个像素的 R 在下标 2 * 3 = 6

所以,对于我们算出来的任意一个逻辑像素 Index,它的 R 通道 在输入数组里的真实下标就是 Index * 3

对于图像中任意一个坐标为 的像素:

  1. 灰度图偏移量 (一维索引): 利用我们学到的展平公式:

  2. RGB图偏移量: 因为每个彩色像素占用 3 个字节,所以在彩色数组中的起始位置为:

有了这两个偏移量,我们就可以读取 R、G、B 的值,并应用经典的心理学灰度公式进行转换:

下面是该转换过程的 CUDA C++ 核函数代码。请注意观察每个线程是如何通过自己的独立坐标来"认领"属于自己的那个像素的。

cpp 复制代码
// CUDA 核函数:彩色图像转灰度
__global__ 
void colorToGreyscaleConversion(unsigned char *Pout, unsigned char *Pin, int width, int height) {
    // 1. 计算当前线程的全局二维坐标
    int Col = blockIdx.x * blockDim.x + threadIdx.x;
    int Row = blockIdx.y * blockDim.y + threadIdx.y;

    // 2. 边界检查:确保线程坐标没有超出图像的实际物理尺寸
    if (Col < width && Row < height) {
        
        // 3. 计算一维线性内存的读写偏移量
        int greyOffset = Row * width + Col;
        int rgbOffset = greyOffset * 3;

        // 4. 从全局内存中读取 R, G, B 三个通道的值
        unsigned char r = Pin[rgbOffset];
        unsigned char g = Pin[rgbOffset + 1];
        unsigned char b = Pin[rgbOffset + 2];

        // 5. 计算灰度值并写入输出数组 (使用浮点数乘法以保证精度)
        Pout[greyOffset] = 0.21f * r + 0.71f * g + 0.07f * b;
    }
}

在上面的核函数中,你可能注意到了一个非常关键的 if (Col < width && Row < height) 语句。为什么我们需要这个边界检查

GPU 的线程块(Block)在各个维度的尺寸通常被配置为 16 或 32 的倍数(例如 ),这是为了迎合底层硬件架构的执行效率。然而,现实中的图像尺寸是任意的(例如 像素)。

如果我们启动尺寸为 的线程块去处理的图像,我们在宽度上需要启动

个块,高度上需要 个块。

这意味着 GPU 实际上启动了一个 的线程网格。那些落在 区域的线程虽然被硬件激活了,但它们并没有对应的图像像素需要处理 。如果让这些"多余"的线程去执行内存读写,就会导致严重的内存越界错误。因此,if 语句是保护内存安全的最后一道防线。

为了确保网格能够完全覆盖整个图像,我们在 CPU 端(Host)需要使用向上取整的数学技巧来计算 gridDim。在 C/C++ 中,我们通常使用一种高效的整数算术技巧 (N + M - 1) / M 来替代浮点数的 ceil() 函数。

在 CUDA 编程里,Host 函数(运行在 CPU 上)就像一个物流调度中心。它有一个非常经典的"五步走"标准工作流:

  • 分配显存 (Allocate):在 GPU 上为输入的彩色图和输出的灰度图腾出合适的空间。

  • 拷贝数据 (Host to Device):把图片数据从 CPU 内存转移到 GPU 显存里。

  • 启动核函数 (Launch Kernel) :呼叫我们刚才写的 colorToGrayscaleConversion,设置好 Grid 和 Block 的维度,让成千上万个线程开始并发计算。

  • 拷回结果 (Device to Host):计算完成后,把 GPU 里生成好的灰度图数据搬回 CPU 内存,以便后续保存或显示。

  • 释放显存 (Free):打扫战场,释放占用的 GPU 空间。

cpp 复制代码
void convertImage(unsigned char* h_rgb, unsigned char* h_gray, int width, int height) {
    // 1. 分配显存 (Allocate)
    unsigned char *d_rgb, *d_gray;
    cudaMalloc(&d_rgb, width * height * 3);
    cudaMalloc(&d_gray, width * height);

    // 2. 拷贝数据 (Host to Device)
    cudaMemcpy(d_rgb, h_rgb, width * height * 3, cudaMemcpyHostToDevice);

    // 3. 设定维度并启动核函数 (Launch Kernel)
    dim3 blockSize(16, 16);
    dim3 gridSize((width + blockSize.x - 1) / blockSize.x, 
                  (height + blockSize.y - 1) / blockSize.y);
    
    colorToGrayscaleConversion<<<gridSize, blockSize>>>(d_rgb, d_gray, width, height);

    // 4. 拷回结果 (Device to Host)
    cudaMemcpy(h_gray, d_gray, width * height, cudaMemcpyDeviceToHost);

    // 5. 释放显存 (Free)
    cudaFree(d_rgb);
    cudaFree(d_gray);
}

对于初学者来说,"多余的线程"是一个很难通过文字想象的概念。

  • 假设我们要处理一张 的微型图像(浅色区域)。

  • 我们配置了 个线程块,每个块是 的线程,总共启动了 个线程的网格。

3.最终进阶:矩阵乘法

如果说"彩色转灰度"演示了点对点 (一个像素对应一个计算)的映射,那么"矩阵乘法"则完美展示了多对一 (一行和一列的数据对应一个输出)的内存访问模式。更重要的是,在第三章讲透基础版(Naive)矩阵乘法,是为后续章节引入"共享内存(Shared Memory)和分块(Tiling)优化"做必须的铺垫。

我们来挑战并行计算领域的经典"Hello World"------矩阵乘法 (Matrix Multiplication)

在深度学习和科学计算中,矩阵乘法是最核心的计算负荷。假设我们需要计算两个方阵的乘积:。其中 的尺寸均为

根据矩阵乘法的数学定义,输出矩阵 中的每一个元素 ,都等于矩阵 的第 行与矩阵 的第 列的点积(Dot Product)

在 CUDA 的多维编程模型中,最自然的做法是:

  1. 启动一个二维的线程网格,使其维度完全覆盖输出矩阵

  2. 让网格中的每一个线程 专门负责计算 中的一个元素

因为矩阵在物理内存中依然是行主序(Row-Major)存储的一维数组,当线程 执行点积运算时,它需要遍历 的一行和 的一列。我们需要一个循环变量 (从)来步进读取数据:

  • 读取矩阵 线程固定在第 行,列索引随 变化。一维内存索引为 Row * Width + k

  • 读取矩阵 线程固定在第 列,行索引随 变化。一维内存索引为 k * Width + Col

  • 写入矩阵 最终结果写回一维内存索引 Row * Width + Col

下面是基础矩阵乘法的核函数代码。请注意观察每个线程是如何通过内部的 for 循环来完成点积运算的:

cpp 复制代码
// CUDA 核函数:基础矩阵乘法 (方阵 P = M * N)
__global__ 
void MatrixMulKernel(float* M, float* N, float* P, int Width) {
    // 1. 计算当前线程负责的输出元素 P 的全局坐标
    int Col = blockIdx.x * blockDim.x + threadIdx.x;
    int Row = blockIdx.y * blockDim.y + threadIdx.y;

    // 2. 边界检查:确保线程坐标在矩阵尺寸范围内
    if (Col < Width && Row < Width) {
        
        float pValue = 0.0f; // 使用局部寄存器累加结果,速度极快
        
        // 3. 执行点积运算:遍历 M 的第 Row 行和 N 的第 Col 列
        for (int k = 0; k < Width; ++k) {
            // 从全局内存读取 M 和 N 的元素
            float mElement = M[Row * Width + k];
            float nElement = N[k * Width + Col];
            
            // 累加乘积
            pValue += mElement * nElement;
        }
        
        // 4. 将最终结果写入全局内存 P 中
        P[Row * Width + Col] = pValue;
    }
}

虽然上述代码逻辑清晰,且完美利用了二维线程模型,但它在实际执行时会面临严重的内存带宽瓶颈 。对于尺寸为 的矩阵,计算每个 的元素需要进行 次全局内存读取。当成千上万个线程同时请求访问全局内存时,GPU 的内存带宽会瞬间被耗尽。如何解决这个问题,将是我们下一章(内存架构与性能优化)的重点。

相关推荐
Elnaij1 小时前
从C++开始的编程生活(22)——红黑树
开发语言·c++
balmtv1 小时前
国内AI镜像站实测:GPT、Gemini、Claude三款旗舰模型技术比拼
人工智能·gpt
ywfwyht1 小时前
nvidia/PhysicalAI-Autonomous-Vehicles数据集
人工智能·自动驾驶
balmtv1 小时前
Claude 4.6国内镜像实测:编程技术硬核拆解
人工智能
南宫乘风1 小时前
Qwen2.5-7B 指令(LoRA)微调完整实战指南
ai·语言模型·训练
Trouvaille ~1 小时前
【项目篇】从零手写高并发服务器(六):EventLoop事件循环——Reactor的心脏
linux·运维·服务器·c++·高并发·epoll·reactor模式
Rorsion1 小时前
对优化器的改进
人工智能·机器学习
学嵌入式的小杨同学1 小时前
STM32 进阶封神之路(十八):RTC 实战全攻略 —— 时间设置 + 秒中断 + 串口更新 + 闹钟功能(库函数 + 代码落地)
c++·stm32·单片机·嵌入式硬件·mcu·架构·硬件架构
学嵌入式的小杨同学1 小时前
STM32 进阶封神之路(十七):RTC 实时时钟深度解析 —— 从时钟源到寄存器配置(底层原理 + 面试重点)
c++·stm32·单片机·嵌入式硬件·mcu·硬件架构·pcb
2601_949221032 小时前
边缘智算加速重构算力格局,微模块技术筑牢低延时基础设施底座
大数据·人工智能·重构