divergence优化和“warp级并行掩盖延迟的能力“权衡

NVIDIA GPU任务调度模型深度解析:从warp到block的硬件调度机制 在操作系统中,CPU调度依赖时间片和优先级,而NVIDIA GPU的调度模型则截然不同:它没有内核态切换,也没有抢占式时间片,而是依靠硬件调度器在大量线程组之间以极低开销切换,从而隐藏内存延迟。理解这一差异,关键在于把握NVIDIA任务调度的核心层次——Grid、Block、Warp和Thread。其中,Warp是硬件真正调度的基本单位,通常由32个线程组成;Block则被分发到流多处理器(SM)上执行。从GigaThread引擎的Block分发,到SM内部Warp调度器的发射逻辑,每一层都深刻影响着CUD 阅读详情

目录

背景

并行归约

优化reduceNeighboredLess

优化reduceInterLeaved

总结


背景

博主也在学习,根据网络上的资料+官方资料+ai分析+个人分析,所以有些观点可能错误,需要深度学习之后建立框架,在回头看现在的观点,可能大家的硬件不一样结果不一样,需要理论+实践进行合理分析,如有错误欢迎大家指正

上一章当中我们学到了一个访存效率的瓶颈,只能达到百分之10左右,核心原因在于L2缓存的不断换入换出,导致缓存命中率降低,从而不断的访问显存,降低了效率

本章将换一个实战场景,不再是简单的矩阵加法,从实战场景引出共享内存

并行归约

矩阵加法 C[i] = A[i] + B[i] 中,A[i] 和 B[i] 都只被读一次,算完就再也不会用了。
共享内存的优势是让一个线程块内的线程可以反复读取同一份数据。如果数据只被用一次,直接从全局内存读一次就够了,搬进共享内存反而多了一次搬运开销,毫无收益。

并行归约就是提供一个数据复用的场景

“归约” 这个词,可以拆成“归”(递归迭代)和“约”(逐渐缩减)意思就是反复用同一种计算方式,把一堆数据一步步缩减成最终的一个结果。这完美契合加法或乘法的结合律和交换律。

任何具有结合性和交换性的计算,都可以使用归约

每次迭代计算方式都是相同的(归),从一组多个数据最后得到一个数(约)

int res=a+b+c+d+e+f+g+h;
//那么可以先算a+b,c+d,e+f,g+h
//然后再算(a+b+c+d)+(e+f+g+h)
//最后由两部分的和得出结果
每一轮的计算方式完全相同,数据量却以对数速度减少(8 → 4 → 2 → 1)。
这种重复使用中间结果的特性,正是共享内存大显身手的地方。

归约的方式基本包括如下几个步骤:

  1. 将输入向量划分到更小的数据块中
  2. 用一个线程块计算一个数据块的部分和
  3. 对每个数据块的部分和再求和得到最终的结果。

1.针对一个大型的数据,可能百万千万上亿级别的计算,一个block最多开1024线程数,不可能用一个block去处理很大的数据量,block和block又是独立的,没不能互相通信,不同的block是在不同的SM上并行的,为了适配SM并行,我们数据就要分块。

2.一个block里面有多个线程,那这些线程就可以通过协作的方式算一个数据块

3.针对block算出来的部分和,然后所有的block算出来的在加在一起,就是结果了

一般来说,算出来block的和之后,我们需要在cpu上加在一起才能得出结果

因为block之间无法通信,所以我们需要移到cpu上进行相加

一般来说得出来的部分和数据就会很小了,比如几百个,你就可以cpu上相加,但是如果数据量还是很大,那就建议在启动一个核函数即可

针对一个加法的场景

归约最常见的就是把数据分对,然后每个线程算一对,得到的结果作为输入继续分成对,迭代的进行,直到最后一个元素

相邻配对:元素与他们的相邻元素进行配对



交错配对:元素与一定距离的元素配对



 

以下代码是cpu上的一个交错配对的并行归约:

  • 每一轮将数组的后半部分元素逐一加到前半部分对应元素上,结果存放在前半部分。

  • 如果数组长度为奇数,单独把最后一个元素加到 data[0]。

  • 然后用新的长度 stride(约原长度的一半)递归调用。

  • 整个过程原地修改输入数组,最终结果保存在 data[0]。

int recursiveReduceCpu(int *data, int const size)
{
    if (size == 1)
        return data[0];
    int const stride = size / 2;
    if (size % 2 == 1){
        for (int i = 0; i < stride; i++)
            data[i] += data[i + stride];
        data[0] += data[size - 1];
    }
    else{
        for (int i = 0; i < stride; i++)
            data[i] += data[i + stride];
    }
    return recursiveReduce(data, stride);
}

下面我们就来通过不同的配对方式,不同的数据组织来看CUDA的执行效率。

之前提到过线程束分化,即使你不执行这个命令,你也要等到这轮结束

橙色圆圈就是每个线程做的操作,可见线程threadIdx.x=0 的线程进行了三次计算,下标奇数线程一致在陪跑,没做过任何计算,但是根据之前的介绍,这些线程虽然什么都不干,但是不可以执行别的指令,4号线程做了两步计算,2号和6号只做了一次计算。

图里的蓝色框,是每一轮被闲置、无有效数据的存储位置

整个朴素归约的过程,全程都是直接在全局内存(显存)里读写数据:

  1. 每一轮线程都从显存里读两个数,算完加法再写回显存,只会操作到部分的内存;
  2. 蓝色框标注的内存,就是只读了一次、后续迭代再也不会被访问的闲置位置 —— 它们还存在显存里,只是逻辑上不再使用。

这种朴素归约的瓶颈就在于你对同一个地方的显存反复利用,你没有加到缓存里面,而是不断的读写显存,性能会降低

//相邻分配
__global__ void reduceNeighbored(int * g_idata,int * g_odata,unsigned int n){
	unsigned int tid = threadIdx.x;             //块内索引
    unsigned int gid=tid+blockIdx.x*blockDim.x; //全局索引
	if (gid >= n) return;                       //越界则退出
	int *idata = g_idata + blockIdx.x*blockDim.x;
	for (int stride = 1; stride < blockDim.x; stride *= 2)
	{
		if ((tid % (2 * stride)) == 0)
		{
			idata[tid] += idata[tid + stride];
		}
		__syncthreads();
	}
	if (tid == 0)
		g_odata[blockIdx.x] = idata[0];
}

threadIdx.x就是这个块内的坐标

idata就是数据要进行分块,你要算这个块内的线程是算哪个数据块

blockIdx.x*blockDim.x就是进行分块用的

下面的stride就是进行步数,这里采用的是相邻分配

加完__syncthreads()保证同一轮内所有线程都完成读写后,再进入下一轮,避免数据竞争。

如果不加__syncthreads()就会出现读写脏数据,因为上一轮有些线程都没算完,你就开始用

这个数据块的结果放到这个块的0号处

接下来就是创建随机数,然后进行测试即可

#include <iostream>
#include "../freshman.hpp"
using namespace std;
int recursiveReduceCpu(int *data, int const size){
    if (size == 1)
        return data[0];
    int const stride = size / 2;
    if (size % 2 == 1){
        for (int i = 0; i < stride; i++)
            data[i] += data[i + stride];
        data[0] += data[size - 1];
    }
    else{
        for (int i = 0; i < stride; i++)
            data[i] += data[i + stride];
    }
    return recursiveReduceCpu(data, stride);
}


//预热函数
__global__ void warmUp(int * g_idata,int * g_odata,unsigned int n){
    unsigned int tid = threadIdx.x;
	if (tid >= n) return;
	int *idata = g_idata + blockIdx.x*blockDim.x;
	for (int stride = 1; stride < blockDim.x; stride *= 2)
	{
		if ((tid % (2 * stride)) == 0)
		{
			idata[tid] += idata[tid + stride];
		}
		__syncthreads();
	}
	if (tid == 0)
		g_odata[blockIdx.x] = idata[0];
}

//相邻分配
__global__ void reduceNeighbored(int * g_idata,int * g_odata,unsigned int n){
	unsigned int tid = threadIdx.x;             //块内索引
    unsigned int gid=tid+blockIdx.x*blockDim.x; //全局索引
	if (gid >= n) return;                       //越界则退出
	int *idata = g_idata + blockIdx.x*blockDim.x;
	for (int stride = 1; stride < blockDim.x; stride *= 2)
	{
		if ((tid % (2 * stride)) == 0)
		{
			idata[tid] += idata[tid + stride];
		}
		__syncthreads();
	}
	if (tid == 0)
		g_odata[blockIdx.x] = idata[0];
}

// 随机初始化整数数组
void initialData(int *ip, int size) {
    srand((unsigned)time(NULL));                    // 设置随机种子
    for (int i = 0; i < size; i++) {
        ip[i] = rand() & 0xFF;                      // 生成 0 ~ 255 之间的整数
    }
}
int main(){
    int dev=0;
    cudaSetDevice(dev);
    int num=1<<24;
    int blocksize=1024;
    dim3 block(blocksize);                          
    dim3 grid((num + block.x - 1) / block.x);


    //开辟内存
    int* tmp=new int[num];
    int* tmpCpu=new int[num];         
    int* tmpGpu=nullptr;
    int* resultGpu=nullptr;
    int* resultGpuToCpu=new int[grid.x];                           //把gpu的结果传输给cpu
    printf("grid.x = %d\n", grid.x);
    CHECK(cudaMalloc((void**)&tmpGpu,sizeof(int)*num));             //开辟显存空间
    CHECK(cudaMalloc((void**)&resultGpu,sizeof(int)*grid.x));        //开辟显存空间
    
    //初始化数据
    initialData(tmp,num);
    
    //启动cpu函数
    memcpy(tmpCpu,tmp,sizeof(int)*num);
    double startCpu,endCpu,sumCpu=0;
    startCpu=efficiency();
    sumCpu=recursiveReduceCpu(tmpCpu,num);
    endCpu=efficiency();
    cout<<"cpu耗时:"<<endCpu-startCpu<<endl;
    
    //先预热
    CHECK(cudaMemcpy(tmpGpu,tmp,sizeof(int)*num,cudaMemcpyHostToDevice));
    double startWarm,endWarm,sumWarm=0;
    startWarm=efficiency();
    warmUp<<<grid,block>>>(tmpGpu,resultGpu,num);
    //等预热结束
    cudaDeviceSynchronize();                                          
    endWarm=efficiency();
    cout<<"warm耗时:"<<endWarm-startWarm<<endl;


    //预热结束启动真正的核函数
    double startGpu,endGpu,sumGpu=0;
    CHECK(cudaMemcpy(tmpGpu,tmp,sizeof(int)*num,cudaMemcpyHostToDevice));
    startGpu=efficiency();
    reduceNeighbored<<<grid,block>>>(tmpGpu,resultGpu,num);
    cudaDeviceSynchronize();    
    endGpu=efficiency();
    //计算完之后拷贝回来
    CHECK(cudaMemcpy(resultGpuToCpu,resultGpu,grid.x*sizeof(int), cudaMemcpyDeviceToHost));
    //cpu最后进行计算总和
    for(int i=0;i<grid.x;i++){
        sumGpu+=resultGpuToCpu[i];
    }
    cout<<"gpu耗时:"<<endGpu-startGpu<<endl;
    cout<<"sumGpu:"<<sumGpu<<endl;
    cout<<"sumCpu:"<<sumCpu<<endl;
    if(sumGpu==sumCpu)
        cout<<"结果正确"<<endl;
      
    else{
        cout<<"结果错误"<<endl;
    }
    return 0;
}

可以看出gpu对于一些简单的运算,并行下还是非常快的

但是以上的朴素归约存在的问题:

1.线程束分化,对于一个加法,只会利用下标为偶数的线程,下标奇数的线程没有用到

2.访存,有些重复利用的空间是否可以加载到缓存当中,没必要每次都访问全局显存

一个block1024的线程,理论上来说需要10次归约才能算出结果,最后的读是2N,写是N,总的读写量达到3N


3.硬件是按照缓存行来读取的,一行是128个字节,即32个int大小,如果stride跨越非常大,到最后可能需要两个事务才能读取
 

之前提到过占有率,占有率=实际的warp数量/理论上SM可容纳的warp数量,无论这个warp里是一个线程再运行,其他31个全部空跑,它还是算一个,所以理论分析这个占有率应该不低,但是实际计算的效率是很低的

基于以上,我们使用ncu进行分析一下

一、线程束分化

指标

含义

smsp__thread_inst_executed_per_inst_executed.ratio

每条指令平均有多少线程真正执行。理想值是 32(一个 Warp 全满)。数值越低,说明越多线程因分化而空转。

smsp__sass_branch_targets_threads_divergent.sum

发生分化的分支指令总条数。这个数字越大,说明代码中导致 Warp 分裂的分支越多。

smsp__sass_branch_targets_threads_uniform.sum

分支目标一致的指令。


二、显存读写数据量

统计内核运行期间实际进出显存的总数据量,用于评估带宽压力和计算效率。

指标

含义

dram__bytes_read.sum

内核执行期间从显存(DRAM)读取的总字节数。

dram__bytes_write.sum

内核执行期间写入显存的总字节数。

gpu__compute_memory_throughput.avg.pct_of_peak_sustained_elapsed

显存吞吐量占理论峰值的百分比。接近 100% 说明带宽被充分利用;较低则可能因访问模式不佳或缓存命中率高而未用满带宽。


三、访存效率 / 合并访问

衡量每次内存请求是否高效。理想情况下,一次请求就能拿到所需数据;非合并访问会导致多次请求,浪费带宽。

指标

含义

l1tex__average_t_sectors_per_request_pipe_lsu_mem_global_op_ld.ratio

每次全局加载请求平均访问的 L1 sector 数。理论最优为 1。数值越大,说明访存越不合并,一次请求被拆成了多次。

同理 ..._st.ratio

写操作的合并程度,含义同上,方向为存储。


四、缓存命中率

反映数据在 L1/L2 缓存中被找到的频率,直接影响数据访问延迟。

指标

含义

l1tex__t_sector_hit_rate.pct

L1 缓存命中率(按 sector 计)。越高越好,意味着更多数据在最近的 L1 中找到,避免了更慢的 L2/DRAM 访问。

lts__t_sector_hit_rate.pct

L2 缓存命中率。越高越好,可以减少对显存的访问。朴素归约中由于全局内存直接操作且无数据复用,L2 命中率通常很低。


五、Occupancy(占用率)

用于排除“Warp 数量不足”这个干扰因素。

指标

含义

sm__warps_active.avg.pct_of_peak_sustained_active

平均活跃 Warp 占比。越高说明 SM 上有越多的 Warp 可供调度。


六、一把梭命令

ncu --set full -o report_xxx ./your_program 会收集所有可用指标并生成一个报告文件(.ncu-rep),非常适合完整分析和截图存档。

词根含义
smStreaming Multiprocessor,流多处理器
smspSM Sub-Partition,SM 子分区
dramDynamic Random Access Memory,显存
l1texL1 Texture Cache,L1 纹理缓存
ltsL2 Cache,L2 缓存
gpu整个 GPU 层面
inst_executed已执行的指令
thread_inst_executed按线程数加权后的已执行指令
sassShader Assembly,GPU 的底层汇编指令
branch_targets分支跳转的目标地址
divergent分化的(线程跳转到不同目标)
uniform统一的(所有线程跳转到同一个目标)
avgAverage,平均值
pctPercentage,百分比
pct_of_peak_sustained_active持续活跃时间占理论峰值的百分比
per_inst_executed按每条已执行指令平均
ratio比率
sum总和
t_sectorTexture Sector,缓存的最小单元
hit_rate命中率
per_request按每次请求平均
pipe_lsuLoad/Store Unit Pipeline,加载/存储单元流水线
mem_global_op_ldGlobal Memory Load Operation,全局内存加载操作
基于以上,我们进行测试


活跃线程占有率是65.51:中等偏上,说明occupancy不是这个kernel的主要瓶颈,可以排除"是因为并行度不够"这个干扰项

访问显存存方面:显存带宽利用率只有24.43%,远没有跑满。这说明瓶颈 不是数据量太大、带宽不够用,而是访存模式本身效率低、没能把带宽用满。

访存合并效率: 理论最优值是1(warp内32个线程的访问刚好落在连续的、最少数量的sector里)。实际是2.67,说明 每次load/store请求平均要触碰2.67个sector ,访存严重不合并。

这是本次实验最关键的发现:idata[tid] += idata[tid + stride]这种写法,随着stride从1递增到512,同一个warp内活跃线程的访存地址间隔越来越大,导致访问越来越分散,无法合并成连续cache line,这直接拉低了显存带宽利用率(呼应上面24.43%这个低数字)。



缓存命中率:L1/L2命中率都不低,说明数据局部性本身还可以(归约过程中访问范围在不断收缩,旧数据容易留在缓存里)。 但这和"访存合并度低"是两件不同的事:命中率高只代表"数据多半能在缓存里找到",不代表"访问请求本身打包得高效"——即使命中缓存,2.67 sector/request依然意味着同一个warp要发出更多次内存事务才能拿到所有线程需要的数据。

线程束分化:
divergent占比 = 3,129,344 / (3,129,344+7,880,704) ≈ 28.4%
线程级有效利用率 = 28.39/32 ≈ 88.7%

结论:存在中等程度的divergence。约28.4%的分支判断在warp内部意见不一致,主要集中在stride较小的早期迭代轮(tid % (2*stride)==0这种写法导致同一warp内只有部分线程满足条件);随着stride增大,越来越多warp整体满足/整体不满足该条件,分化逐渐减少。这一点divergence是性能损耗的一部分,但不是最大头,最大头应该是访存。

取模分支判断 (tid % (2*stride)==0)
        │
        ├──→ 28.4%分支divergent,线程级利用率降到88.7%   ← 性能损耗来源①
        │
        └──→ 活跃线程访存地址间隔不连续 (stride递增)
                    │
                    └──→ sector/request = 2.67(理论最优1)  ← 性能损耗来源②(更大头)
                                │
                                └──→ 显存带宽利用率仅24.43%(远未跑满)
                                         (即使L1/L2命中率高达91.6%/84.4%,
                                          依然无法弥补访问模式本身的低效)
 

综合结论

  1. 两个理论瓶颈都被数据证实,但访存不合并的影响权重明显大于divergence:

  2. Divergence造成约11.3%的线程级浪费(28.39 vs 32)
  3. 访存不合并造成sector利用率仅37.5%(1/2.67),且最终反映为带宽利用率不到1/4
  4. Occupancy 65.51%说明GPU没有闲着,有足够warp可调度,进一步印证瓶颈确实是"访存效率"和"指令利用率"这两个kernel内部的问题,而不是"并行度不够掩盖延迟"的问题。

优化reduceNeighboredLess

基于以上我们主要发现两个问题:

1.访存效率

2.线程束分化


太多的线程用不上,并且这些线程还必须等待这一轮指令的完成

  • tid=0,2,4,6 工作;tid=1,3,5,7 摸鱼
  • 工作的线程ID是离散的、间隔分布的:0, 2, 4, 6

优化:

还是采用相邻分配,但是这次我们让warp前面的线程工作,后面的线程不工作,把工作的集中到一块,也就是0123好线程工作,4567号线程不工作

__global__ void reduceNeighboredLess(int * g_idata,int *g_odata,unsigned int n)
{
	unsigned int tid = threadIdx.x;
	unsigned idx = blockIdx.x*blockDim.x + threadIdx.x;
	int *idata = g_idata + blockIdx.x*blockDim.x;
	if (idx > n)
		return;
	for (int stride = 1; stride < blockDim.x; stride *= 2){
		
		int index = 2 * stride *tid;
		if (index < blockDim.x){
			idata[index] += idata[index + stride];
		}
		__syncthreads();
	}
	if (tid == 0)
		g_odata[blockIdx.x] = idata[0];
}

工作的线程ID变成了连续的:tid=0,1,2,3;不工作的也是连续的:tid=4,5,6,7。
空闲线程数量没有减少。 真正变化的是空闲线程在warp内的排列方式。
那么放大来看,你的数据量可是1<<24这么大,一个线程束仍然32个线程,一个block里面有32个线程束,即1024个线程,那么这个block在执行的时候,我们把工作的线程集中在前面0-511,不工作的线程集中在后面512-1023,那么不工作的线程是连续的在同一个warp里面,那么是不是不会分化了
换句话说:divergence只会发生在边界所在的那一个warp,其余warp是uniform的。而原版几乎每一个warp(只要stride不太大)都会发生divergence,因为交错模式让每个warp内部都同时存在工作/摸鱼线程。

第一张图:快了将近一倍,但需要跑多次取平均值,这里博主只跑了几次,大概也是0.7,0.8倍左右

第二张图:

最主要的先看分支:完全符合理论预期。把交错分布的工作线程改成连续聚集分布后,divergence从"几乎每个warp都中招"降到"只有极少数边界warp",线程级利用率从88.7%提升到99%。这是这个优化版本最干净、最直接的收益,证据非常扎实。(之前的分支大概是300多万条,这里仅仅有8万多条)
 

带宽利用率提升了约62%(相对值),这个提升不是来自访存变得更合并(马上会讲为什么),而主要来自:divergence减少后,SM的指令发射效率提高,单位时间内能发出更多有效访存请求,从而在相同时间窗口内推送更多数据,带宽利用率随之被"挤"上去了。

访存其实是一样的,sector的总量基本一致,主要是request数量不一样,这个比值是通过sector/request算出来的,优化版本的request更少,所以比值高
最重要的是要清楚第二个版本的一个warp的request更重,他要申请的sector更多

两者最终带宽利用率:27.08% → 43.92%,净效果是提升的,说明在这个case里,"消除divergence带来的指令效率提升"完全压过了"单次request变重"带来的效益。

这是一个很好的论点:本次实验两个瓶颈不是独立可加的,优化一个可能会让另一个变差,最终结果取决于谁的影响权重更大。
reduceNeighboredLess通过将线程到数据的映射方式从"交错离散"改为"连续聚集",几乎完全消除了warp divergence(divergent分支占比从28.4%降至0.74%,线程级有效利用率从88.7%提升至99.0%)。然而,这一映射方式同时带来单次的request变重

总结

  • 朴素归约有两个独立瓶颈:divergence(线程行为不一致)+ 访存效率(地址跨度大、不合并)
  • reduceNeighboredLess只改变了"哪些tid在工作"的分布方式(让活跃tid连续),没有改变地址的映射逻辑本身,所以:
    • Divergence问题解决了(因为divergence看的是warp内行为是否一致,活跃tid连续=同一warp内行为一致)
    • 访存效率问题不仅没解决,还加剧了单次request,最终等待时间可能会变久
  • 综合带宽利用率提升,不是因为"访存变合并了",而是因为"divergence消除后,请求发出的频率/效率提升了",这是两个独立维度的此消彼长,本次实验中"频率提升"的收益压过了"单次效率下降"的损失

优化reduceInterLeaved

采用交错配对

//交错分配,线程uniform
__global__ void reduceInterleaved(int *g_idata, int *g_odata, unsigned int n) {
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int *idata = g_idata + blockIdx.x * blockDim.x;
    if (idx >= n) return;

    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) {
        if (tid < stride) {
            idata[tid] += idata[tid + stride];
        }
        __syncthreads();
    }
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

为什么这个可以合并???

首先要搞清楚

  • A. 一个线程访问的两个地址之间的距离(即idata[tid]和idata[tid+stride]之间隔多远)
  • B. 同一个warp内,32个线程访问的起始地址,相互之间是否连续

我们主要看得是B这种情况,而不是A,因为访问连续我们才可以合并,为什么上一轮合并效果不好,因为访问的是idata[0] [2] [4] [6]这样,效果怎么会好

而这一轮,idata[tid]这部分,32个线程访问的是idata[0..31]——这32个地址是完全连续的! 这就是一次标准的合并访问,32×4字节=128字节,对应4个sector,效率很高。

idata[tid+512]这部分,32个线程访问的是idata[512..543]——这也是连续的32个地址! 同样是一次完美合并访问。


idata[tid] += idata[tid + stride];最重要的就是这条

你每次相隔stride跨度,但是你每次的idata都是连续的128字节的地址

所以这样分配的好处可以访存合并

需要注意这个比值效率来说是:sector ÷ request

为什么第一次是2.67,因为它后面跨度非常大之后,导致一个线程束可能发出一次请求用一个sector就行,所以导致整体的ratio会变小,而后续测出来的3.58可以说明访存合并效果更好,一次访存需要差不多4个sector

把sector/request和gpu内存吞吐结合起来看,才是完整的故事:

  • sector/request 衡量"单次请求的装载效率"(这次request有没有把sector用满),但它的分母会受divergence影响而失真,不能孤立地只看这一个数字判断好坏
  • gpu__compute_memory_throughput 衡量"整体数据搬运的吞吐总量",这个才是最终关心的"性能好不好"的直接体现

reduceInterleaved同时做到了:divergence很低(thread_inst_executed_per_inst_executed=31.67,接近满载32)+ sector/request合理(3.58,接近理论基准4,是健康的合并访问,不是因为divergence被稀释出来的假低值)+ 吞吐总量最高(42.17%,三个版本里仅次于reduceNeighboredLess的47.56%,但更健康)。

虽然reduceNeighboredLess的47.56%高于reduceInterLeaved的42.17%,但是你搬运了很多垃圾值,而reduceInterLeaved虽然搬运的总量小,但是搬运的有用值多于reduceNeighboredLess,导致最后的时间上效果更好

总结

所有的实际情况来说,每个人的应用场景不同,硬件不同,测出来的效果都不一样

  • 理论分析:从代码逻辑出发,预判可能存在的瓶颈(一开始就判断出divergence和访存效率两个候选瓶颈,这一步做得很好)
  • 指标验证:用ncu具体测出每个理论瓶颈的严重程度,量化"到底有多差"
  • 实测验证:用实际运行耗时(wall-clock time)确认优化是否真的让程序变快了,因为指标只能解释现象,不能替代真实的性能结果

这里主要注意第二个版本,减少线程束分化带来了带宽的提升,你搬运数据的总量得到了提升,但是你同时每次搬运sector/request垃圾值过多,但是这个实验来看,减少线程束分化带来的效益比较高,实际我们应该根据场景来判断
reduceInterleaved不是银弹,它只是这次实验范围内的阶段最优解,更进一步还有shared memory、warp shuffle等技术

通过学习,博主还发现一个结论,但是不明确是否正确,主要是第一个和第二个版本

其实分析来看都是相邻分配,sector总量应该来说是一样的,但是request不一样,因为第二个版本把工作线程压缩在一个warp里面,那就会导致发出的request更少

那么会不会存在一些特殊的情况导致第一个版本实际优于第二个版本呢?如果你的warp发出请求的时候,很慢,会阻塞,那就需要调度其他的warp上来,但是发现如果此时没有可用的,那就会等,所以效率就很慢

反观第一个版本,虽然你发出的request变多了,但是每次都很轻量,所以很快能返回,而且你是把每次都分摊在多个warp上面,所以如果有一个慢,那就调度别的上来运行

设想一种情况,如果你的数据量很大,那么你越到后面第二个版本的warp会很重,等很久又没有别的调度,但是第一个版本的warp很轻,所以会不会变快呢?总之就是用更多轻量的warp去提高并发度

但是还得看吞吐量,第二个版本减少了线程束分化还带来了吞吐量的提升

其实本质来说是上节学的隐藏延迟

上手实验:

这里不用管结果,可能是因为溢出还是啥吧,但是可以看出第二个版本比第一个版本慢太多了,这里的数据量是1<<30次方

当数据量足够大时,reduceNeighboredLess后期"重warp、少并发"的劣势会被放大,最终反超reduceNeighbored,变成更慢的版本。
等待的时间变长了,导致你的带宽效率都下降了,时间线拉长再加上ncu,效率变低了

所以 
 

  • 小数据规模(1<<24)下:divergence是主导瓶颈,reduceNeighboredLess和reduceInterleaved都显著优于reduceNeighbored,吞吐量和实测耗时排序基本一致
  • 大数据规模(1<<30)下:reduceNeighbored反而实测最快,reduceNeighboredLess明显最慢,说明divergence优化的收益和"warp级并行掩盖延迟的能力"之间存在权衡,数据规模是影响这个权衡天平倾向哪一边的关键变量
  • 吞吐量百分比和实测耗时的排序在大数据量下出现了不一致,说明在grid规模极大、调度压力本身成为显著因素时,ncu的吞吐量指标不能单独作为效率判断依据,必须结合实测耗时

所以没有任何一种技术是银弹,必须结合实际

英伟达暑期实习笔试全解析:并行计算与深度学习底层机制 并行计算通过多线程协同提升计算效率,其核心在于线程层次结构与内存管理。GPU利用大量线程并行处理任务,warp调度和shared memory优化是关键。深度学习依赖并行计算加速卷积和反向传播,计算量与内存访问模式影响性能。在英伟达实习笔试中,重点考察CUDA线程组织、bank conflict、occupancy等底层机制,以及CNN计算量估算和优化器选择。掌握这些原理,能有效应对笔试中的工程场景分析。 阅读详情

相关推荐

仿真性能优化五大核心资源:从诊断工具到并行框架的工程实践

仿真(Simulation)作为驱动工业研发与科学发现的核心引擎,其性能直接决定了项目的效率与成本。性能瓶颈的本质通常源于计算、通信与I/O三个层面的不平衡,遵循木桶原理。要系统性地提升仿真效率,工程师需要掌握从底层硬件调优到上层算法选型的一系列关键技术。在工程实践中,专业的性能剖析工具(如perf、Intel VTune)和高效的数学计算库(如Intel MKL、OpenBLAS)是定位热点与加速计算的基础资源。同时,针对并行仿真场景,合理运用MPI、OpenMP及CUDA等并行计算框架,并优化HDF5等

weixin_33743703的博客 433

CUDA第一性原理:从GPU硬件架构到PTX汇编的并行计算重建

并行计算不是简单地增加线程数,而是对计算、内存与调度的协同建模。GPU并行的本质源于SM(流式多处理器)的物理结构——寄存器文件容量、32-bank共享内存、WARP调度机制共同决定了线程并发上限与访存效率。理解coalesced memory access(合并访问)和branch divergence(分支发散),需回归到硅基硬件约束:如L2缓存带宽是HBM2的16倍,却受共享内存/L1配置牵制;如PTX中`ld.global.f32`指令延迟达400周期,必须靠计算掩盖。这些并非调优技巧,而是Roof

cuiyingchan0663的博客 443

GPU 与 CPU 架构差异

CPU与GPU的设计目标本质不同:CPU优化单任务延迟(Latency-oriented),通过乱序执行、分支预测和大缓存加速;GPU优化吞吐量(Throughput-oriented),以大量简单核心和轻量级线程(Warp)并行处理同类任务,通过线程切换掩盖延迟。SIMT模型将线程分组为Warp(32线程),以SIMD方式执行,允许编程时写标量逻辑,但需避免分支发散(Warp Divergence)。

bug_love的博客 80

深度解析NVIDIA GPU任务调度:从Block分发到Warp发射的完整链路

在并行计算性能优化中,理解GPU的任务调度机制是解锁硬件潜力的关键。CUDA程序性能不仅取决于kernel实现和显存带宽,更受制于从CPU端命令队列到SM内部Warp调度器的多级调度链路。NVIDIA GPU采用分层队列模型,通过GigaThread Engine完成线程块分发,利用Warp调度器实现指令级并发。掌握Occupancy计算、启动开销控制、延迟隐藏等核心技术,能有效解决尾效应、SM利用率不足、Warp停滞等常见性能瓶颈。本文从硬件架构出发,结合CUDA编程实践,系统讲解任务调度原理与优化方法,

weixin_33853794的博客 374

CUDA 11.0编程指南实战:线程层次、内存优化与性能调优

GPU并行计算正成为高性能计算与深度学习领域的关键技术底座,而CUDA作为最主流的并行编程框架,其核心价值体现在对硬件资源的精细控制上。理解CUDA编程模型中的线程层次与内存层次,是写出高效核函数的前提——线程如何组织为grid和block、数据如何在全局内存与共享内存间流转,直接决定了程序的执行效率和带宽利用率。内存合并访问、共享内存分块、占用率权衡等优化手段,则是在实践中反复验证的通用性能准则。这些原理不仅适用于科学计算、图像处理,也广泛支撑着AI推理加速和工业仿真场景。本文基于CUDA 11.0官方编

weixin_32183107的博客 254

若依集成mybatisplus、兼容旧分页【亲测有效】

service类继承ServiceImpl类 实现 你的Service接口。hutool工具包和lombok工具包如果不需要的话可以自行删除并修改后面的类里面的代码。自带的pagehelper用的1.4.6里面的jsqlparser用的4.5.0。mybatis-plus用的3.4.2里面的jsqlparser用的4.0.0。jsqlparser包,不剔除是这样的,有个冲突,我用的前端写的请求,返回的格式和原来的一样,已兼容。你的Entity类上面需要增加表的名字。

lh155136的博客 3566

CUDA性能本质:从GPU硬件架构到内存与线程协同优化

GPU并非更快的CPU,而是一种以数据并行和延迟隐藏为核心设计的吞吐密集型计算架构。其性能上限不由算力峰值决定,而由内存层次(global/shared/constant)、访存模式(coalescing)、线程调度单元(warp)及PCIe与显存带宽瓶颈共同约束。理解SM中warp切换机制、shared memory作为协作总线而非缓存的本质、以及cudaMemcpy等数据搬运操作的纳秒级硬件语义,是突破PyTorch底层性能天花板的关键。本文聚焦CUDA编程范式重构,结合Nsight实测诊断,系统解析l

weixin_34274029的博客 435

Palantir Vertex:数字孪生模拟与决策系统深度指南

Palantir Vertex是Foundry系统中的核心数字孪生模块,本质上是一个决策模拟器而非简单的数据可视化工具。它基于本体层构建,通过对象关系网络模拟现实世界实体,填补数据模型与业务决策间的鸿沟。核心功能包括:创建隔离的沙盒场景进行假设测试、修改参数模拟不同干预方案、对比虚实数据发现异常,并最终将最优决策回传至业务系统。典型应用场景涵盖供应链韧性评估、资产维护优化和应急方案制定等,帮助用户在复杂环境中做出更优决策。

轩哥的个人研究和成长记录 891

2018商汤GPU优化工程师笔试复盘:从CUDA到性能优化的核心考点

在并行计算与高性能计算领域,GPU优化是决定深度学习训练效率的关键环节。CUDA作为GPU编程的核心模型,其线程组织、内存层次与访存行为直接影响kernel性能。理解warp调度、共享内存、bank conflict等概念,是进行性能优化与定量分析的基础。Roofline模型、占用率计算、访存带宽估算等方法,帮助工程师快速定位计算密集与访存密集瓶颈。这类能力在AI基础设施、算子优化等岗位的笔试面试中尤为重要。本文以2018年商汤科技校招GPU优化工程师第一场笔试为例,复盘其考点分布与出题逻辑,展示从通用并行

471

【ISP图像处理】Demosaic去马赛克技术演进与边缘优化策略

本文深入解析了ISP图像处理中的Demosaic(去马赛克)技术。从Bayer阵列原理出发,梳理了从双线性插值、Hamilton-Adams边缘导向算法到利用通道相关性与自相似性的技术演进历程,并重点探讨了针对边缘与纹理伪影的优化策略,为工程实践中的算法选型与硬件协同设计提供了专业指导。

weixin_29091445的博客 388

深入浅出ISP:用RKISP2.x Tuner工具理解AWB、AE、HDR等核心模块的实战调参

本文深入解析RKISP2.x Tuner工具在ISP图像处理中的实战应用,重点探讨AWB、AE、HDR等核心模块的调参技巧。通过环境搭建、调试工具使用和实际案例,帮助开发者掌握如何在不同光照条件下优化图像质量,实现精准的白平衡、曝光控制和动态范围处理。

weixin_26802933的博客 487

商汤GPU优化笔试全解析:从CUDA编程到性能调优

并行计算是高性能计算的核心,而GPU作为并行计算的主流载体,其性能优化直接关系到深度学习训练与推理的效率。GPU优化工程师需要深入理解CUDA编程模型和GPU硬件架构,掌握线程组织、内存层次、访存合并等关键技术。面对计算密集或访存密集的任务,工程师需通过shared memory tiling、向量化、调整occupancy等手段突破性能瓶颈。借助Nsight等性能分析工具定位问题,是工程实践中不可或缺的能力。商汤科技2018年GPU优化工程师笔试题正是对这一底层能力的综合检验,本文以其为线索,梳理从硬件原

weixin_34009794的博客 445

波动性GARCH模型与波动率预测(代码+结果分析)

本文介绍的ARCH、GARCH模型可以刻画出随时间变化的条件异方差。

celiaweiwei的博客 2万+

乱序执行在NPU/GPGPU中的价值与工程选择:从浅度调度到动态并行

乱序执行是现代处理器挖掘指令级并行的重要手段,通过动态调度指令发射顺序来掩盖延迟、提升执行单元利用率。随着AI加速器规模不断膨胀,传统顺序发射加静态编排的方式在应对复杂负载时逐渐力不从心。从FLOPs利用率出发,可以清晰看到NPU与GPGPU中引入乱序调度的必要性与实现形态,涵盖浅度乱序、寄存器重命名改造、独立线程调度等关键技术,并结合稀疏计算、动态形状等非规则负载,探讨乱序执行带来的实际收益与工程代价。分阶段演进的实践建议,为设计者在能效与性能之间做出平衡提供了可行路径。

weixin_34126557的博客 399

新手必看!无人机定高PID调参避坑指南:从理论到实战的完整流程

本文为无人机新手提供定高PID调参的完整指南,从传感器融合到抗风策略,详细解析多传感器融合、PID控制参数调试及环境干扰应对。通过实战案例和代码示例,帮助开发者快速掌握无人机定高技术,避免常见调参陷阱,提升飞行稳定性。

spark7igniter的博客 221

人工智能数据治理——解读AI时代数据治理白皮书 2025【附全文阅读】

摘要:该白皮书面向企业数字化负责人及AI项目推进人员,探讨AI时代数据治理的战略升级。提出"好数据+好数据知识"双基石理念,强调数据需满足"三高"标准(高精度、一致性、时效性)并构建语义知识库。构建四层治理框架,包括数据采集处理、合规安全、知识库建设及质量改进,并介绍AI赋能治理的新范式,如智能ETL和治理Agent体系。同时分析"软件+AI+好数据"的应用模式,为企业实现智能数据治理提供实践指导。(149字)

cdfunlove的博客 633

英伟达暑期实习笔试全解析:CUDA编程、深度学习算子与GPU计算量考点

GPU并行计算与CUDA编程模型是高性能计算和深度学习底层开发的核心基础。其原理在于通过线程层次结构、内存层次和同步机制,实现大规模数据的高效并行处理。掌握这些技术能显著提升模型训练与推理性能,广泛应用于算子开发、框架优化和显存管理。英伟达暑期实习笔试正是围绕CUDA编程、深度学习算子计算量及显存占用估算等高频考点展开,深入理解这些内容有助于应对相关岗位的校招挑战。

weixin_34301132的博客 577

复旦微FMSH_Procise安装全流程详解:从环境配置到实战避坑指南

通过本文的全流程解析+实战技巧,开发者可快速掌握Procise在复旦微芯片开发中的核心应用。未来随着AKConv轻量级架构(参考往期文章)等创新技术的融入,国产芯片开发生态将更加完善。立即关注本专栏,获取最新多镜像切换指南与Linux SDK实战教程!扩展阅读复旦微在线升级BOOT.BIN全解析H7-TOOL实现复旦微脱机烧录(注:本文实验环境为Windows 10 x64 + FMQL45T-CORE开发板,部分操作可能因版本更新调整)立即行动下载本文配套配置文件:[GitHub链接]加入技术交流群。

QQ_778132974的博客 3240

AI Infra面试必备:25个CUDA高频面试题深度解析

GPU并行计算是深度学习的算力基石,其核心在于通过CUDA编程模型实现大规模线程级并行。理解线程层次结构、内存层次与warp调度机制,是编写高性能kernel的关键。掌握这些原理后,开发者能有效优化归约、矩阵乘法等算子,并借助Tensor Core与FlashAttention等技术突破显存与带宽瓶颈。在AI Infra、HPC及推理优化岗位面试中,CUDA能力直接决定候选人能否将模型高效部署到GPU。本文围绕25个高频面试题,从基础概念到FlashAttention逐层拆解,帮助读者构建系统化的CUDA知

weixin_33139275的博客 176

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

GPU架构深度解析:从CUDA核心到并行计算实战

图形处理器(GPU)从诞生之初就为高吞吐并行计算而生,与以低延迟为目标的CPU在设计哲学上截然不同。理解GPU架构的关键,在于把握它如何用成千上万个精简计算单元,通过SIMT模型与Warp调度实现极限并行。Tensor Core、共享内存与访存合并等机制,则让矩阵运算、AI训练等数据密集型任务获得数量级加速。在实际工程中,无论是配置PyTorch GPU版本,还是排查GPU发生崩溃或D3D设备已移除等驱动问题,本质上都与计算能力匹配、资源占用和访存模式紧密相关。随着大模型与异构计算普及,掌握GPU架构原理已

weixin_34166472的博客 348

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

GPU指令执行机制解析:从ALU到寄存器,深入UE Shader优化

在GPU渲染中,Shader性能取决于底层指令执行效率。GPU采用SIMT架构,通过ALU执行运算,寄存器存储数据,线程调度隐藏延迟。理解这些机制,能帮助开发者优化Shader,减少指令数和寄存器压力,提升移动端帧率。在UE中,材质节点最终编译为GPU指令,优化需关注指令数、寄存器数和占用率。应用场景包括移动端游戏、实时渲染等。本文从GPU执行模型切入,结合UE Shader实战,解析优化策略。

weixin_34326179的博客 329

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

ComputeShader性能优化实战:从内存访问、线程调度到移动端适配

GPU并行计算是现代图形与通用计算的核心技术,其性能优化关键在于理解硬件执行原理。GPU采用SIMD架构,以线程束(Warp/Wavefront)为调度单位,线程束内分支发散会严重降低并行效率。内存访问模式直接影响带宽利用率,合并访问与共享内存的正确使用能大幅减少延迟。计算强度(Arithmetic Intensity)决定了ALU利用率,需平衡计算与访存。在移动端优化中,半精度浮点数(half)和寄存器压力管理尤为重要。本文聚焦ComputeShader性能调优,通过解析线程组大小选择、共享内存归约等实战

weixin_34252686的博客 350

深入GPU SM架构与CUDA编程:从黑盒子到高性能计算实战

在并行计算和高性能计算领域,理解底层硬件架构是优化程序性能的关键。GPU(图形处理器)作为现代计算加速的核心,其流式多处理器(SM)架构决定了大规模并行任务的处理效率。SM内部包含CUDA核心、张量核心、寄存器文件和共享内存等精密单元,通过线程束(Warp)调度实现指令级并行。从原理上看,SM采用SIMT(单指令多线程)执行模型,通过隐藏内存延迟和最大化计算单元利用率来提升吞吐量。这一架构的技术价值在于为人工智能训练、科学模拟和图形渲染等数据密集型应用提供了强大的算力基础。在实际应用场景中,开发者通过CUD

weixin_34166472的博客 465

工业界真实版「算子开发工程师技能树」:从 CUDA Kernel 到 AI 编译器与硬件映射

算子开发工程师是AI与高性能计算的交叉岗位,核心能力包括计算机体系结构、并行计算模型、线性代数等硬核基础。核心技能涵盖GPU Kernel编程(CUDA/Triton/CUTLASS)、循环调度设计、内存布局优化等。高阶能力包括算子多实现策略、Kernel融合、自动调优及硬件映射(如数据流优化)。工具链涉及性能分析(nsys/nvprof)和IR(MLIR/TVM)。LLM时代新增FlashAttention、KV缓存等专项技能。行业能力分级从基础CUDA编程到架构级编译器设计,反映了从入门到专家的成长路径

Qnesp的博客 508

深入GPU核心架构:SM、Tensor Core、HBM与NVLink原理与AI性能优化实战

GPU作为现代AI计算的核心硬件,其性能表现由内部多个子系统协同决定。理解其底层架构原理是进行高效计算优化的基础。从计算原理层面,GPU采用SIMT(单指令多线程)架构,通过SM(流式多处理器)组织成千上万个CUDA核心进行大规模并行计算。在AI计算场景中,专用硬件单元Tensor Core通过混合精度计算实现了矩阵运算的吞吐量飞跃,成为大模型训练的关键加速器。而HBM(高带宽内存)技术通过2.5D/3D堆叠架构解决了“内存墙”问题,为计算单元提供高速数据供给。在多GPU协同工作时,NVLink点对点互联技

weixin_33797791的博客 392

CPU与GPU架构差异解析:从矩阵乘法看并行计算与AI加速原理

并行计算是现代高性能计算的核心概念,它通过同时执行多个计算任务来大幅提升数据处理吞吐量。其原理基于任务分解与同步执行,在硬件层面体现为多核心架构与专用计算单元的设计。这种技术价值在于能够高效处理海量数据,特别适用于科学计算、图形渲染和人工智能等数据密集型场景。以矩阵乘法为例,CPU采用多线程和SIMD指令进行优化,而GPU则通过SIMT架构和上万计算核心实现大规模并行。在深度学习模型训练中,GPU的并行能力成为加速神经网络前向与反向传播的关键,而CPU则负责逻辑控制与数据调度,两者协同构成高效的异构计算系统

weixin_34014555的博客 370
上一篇: 并行性的表现:理论和实际是有差别的
下一篇: GPU性能优化:展开归约压榨最后一滴性能
Fcw0551
博客等级 码龄2年 330粉丝 115原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值