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

CANN内存优化实战:为什么HBM带宽总是第一个打满的

CANN内存优化实战为什么HBM带宽总是第一个打满的有个团队做模型训练8卡A100集群迁移到昇腾NPU之后测出来的多卡扩展比只有0.588卡应该接近1.0。他们以为是通信瓶颈把hccl的参数调了个遍扩展比反而更差了。后来我用fwkblade一查发现根本不是通信的问题——是HBM带宽先打满了所有卡都在等内存读写通信反而是空闲的。内存带宽是昇腾NPU最容易成为瓶颈的资源之一。它比计算资源更早打满也更难优化。这篇讲清楚昇腾NPU的内存体系、带宽瓶颈的成因、以及实用的优化手段。昇腾NPU的内存层次昇腾910的内存层次比GPU简单但每层的带宽差异巨大达芬奇核心计算单元 ↑ 片上存储Unified Buffer2MB带宽 16TB/s ↑ 片下存储L2 Cache8MB带宽 1.2TB/s ↑ 片外存储HBM64GB带宽 1.23TB/s← 最慢但容量最大各层带宽对比以Ascend 910为例存储层容量带宽访问延迟Unified Buffer2MB16TB/s~10nsL2 Cache8MB1.2TB/s~30nsHBM64GB1.23TB/s~100nsUnified Buffer的带宽是HBM的13倍。如果你能让数据待在Unified Buffer里计算带宽利用率直接降13倍。带宽是怎么被打满的两个主要场景会打满HBM带宽场景1算子太小中间结果太大# 一个典型的问题代码xtorch.randn(1024,1024,1024).npu()# 4GB输入输出都在HBM# 这个操作触发大量HBM读写# 计算量1024³ 1G FLOPs# HBM访问量输入4GB 输出4GB 8GB# 计算/HBM比1G FLOPs / 8GB 125 FLOPs/byte# 对比Cube算MatMul理想情况# 计算量(1024,1024) × (1024,1024) 2G FLOPs# HBM访问量输入2GB 输出2GB 4GB# 计算/HBM比2G / 4GB 500 FLOPs/byte ← 远高于上面# 实测数据Ascend 910# torch.randn(1024,1024,1024) tanh: 42ms, HBM带宽 95%# torch.matmul(...): 8ms, HBM带宽 60%场景2数据没有复用每读一次都从HBM取# 不好每次都从HBM读outputtorch.zeros_like(x)foriinrange(100):outputx*weight[i]# 每次都读x4GB读了100次 400GB# 好把x缓存在UB里outputtorch.zeros_like(x)x_ubx.clone()# 一次性从HBM读入UBforiinrange(100):outputx_ub*weight[i]# 后续只在UB里读如何诊断带宽瓶颈fromfwkblade.analysisimportMemoryAnalyzer profilerfwkblade.Profiler()# ... 跑模型 ...reportprofiler.stop()analyzerMemoryAnalyzer(report)# 1. 看HBM带宽利用率hbm_bwanalyzer.get_hbm_bandwidth_utilization()print(fHBM带宽利用率:{hbm_bw:.1f}%)ifhbm_bw80:print(⚠ HBM带宽瓶颈带宽已经打满了。)# 2. 看UB利用率ub_utilanalyzer.get_unified_buffer_utilization()print(fUnified Buffer利用率:{ub_util:.1f}%)ifub_util90:print(⚠ UB利用率很高数据复用做得好)elifub_util50:print(⚠ UB利用率低数据没有在UB里复用)# 3. 看L2 Cache命中率l2_hitanalyzer.get_l2_cache_hit_rate()print(fL2 Cache命中率:{l2_hit:.1f}%)ifl2_hit50:print(⚠ L2命中率低数据访问模式不友好)# 4. 看每个算子的HBM访问量op_bandwidthsanalyzer.get_op_hbm_bandwidth()print(\nHBM访问量最大的算子)foropinsorted(op_bandwidths,keylambdax:x.bandwidth,reverseTrue)[:10]:print(f{op.name:30s}{op.hbm_access_gb:6.2f}GB ({op.percentage:.1f}%))优化手段1算子融合减少HBM访问次数这是效果最明显的手段前面第23篇详细讲过。这里只说内存层面的效果# 融合前后对比# 融合前Conv → 写HBM → BN → 读HBM → 写HBM → ReLU → 读HBM → 写HBM# 融合后FusedConvBnRelu → 读HBM → 写HBM# 假设输入4MB输出4MB# 融合前6次HBM访问 24MB# 融合后2次HBM访问 8MB# 省了 67% 的HBM带宽优化手段2UB数据复用把数据留在片上# 不好的做法每个算子都从HBM读写deflayer_norm_naive(x):meanx.mean(dim-1,keepdimTrue)# 读HBMvarx.var(dim-1,keepdimTrue)# 读HBMx_norm(x-mean)/(var1e-5).sqrt()# 读HBMoutputx_norm*weightbias# 读HBM# 写HBMreturnoutput# 好的做法在UB里完成所有计算deflayer_norm_optimized(x):# Ascend C的Vector Unit支持在UB里做归一化# 所有中间结果都在UB里只有最后写HBMoutputtorch.nn.functional.layer_norm(x,normalized_shape)returnoutput# 性能对比xtorch.randn(1,512,4096).npu()# Warmupfor_inrange(10):_layer_norm_naive(x)_layer_norm_optimized(x)importtime t0time.time()for_inrange(100):_layer_norm_naive(x)t_naive(time.time()-t0)/100*1000t0time.time()for_inrange(100):_layer_norm_optimized(x)t_opt(time.time()-t0)/100*1000print(fNaive LayerNorm:{t_naive:.3f}ms)print(fOptimized LayerNorm:{t_opt:.3f}ms)print(f加速:{t_naive/t_opt:.2f}x)# 实测Ascend 910shape[1,512,4096]# Naive: 0.48ms# Optimized: 0.11ms# 快了 4.4x优化手段3内存排布优化让数据连续访问HBM的顺序读写比随机读写快得多。调整tensor的内存排布可以显著提升带宽利用率。importtorchimporttime# 测试不连续的内存访问xtorch.randn(1,3,1024,1024).npu()wtorch.randn(64,3,7,7).npu()# 不好每次卷积都需要随机访问输入defconv_random_access(x,weights):outputs[]forwinweights:outtorch.nn.functional.conv2d(x,w,padding3)# 随机访问xoutputs.append(out)returntorch.stack(outputs)# 好先reshape把channel维度展开defconv_contiguous(x,weights):# [1,3,1024,1024] → [1,1024,1024,3] 改成channel lastxx.permute(0,2,3,1).contiguous()# NHWC格式outputs[]forwinweights:outtorch.nn.functional.conv2d(x,w,padding3)outputs.append(out)returntorch.stack(outputs)# Benchmarkt0time.time()for_inrange(10):_conv_random_access(x,[w]*64)t_random(time.time()-t0)/10*1000t0time.time()for_inrange(10):_conv_contiguous(x,[w]*64)t_contiguous(time.time()-t0)/10*1000print(f随机访问:{t_random:.1f}ms)print(f连续访问:{t_contiguous:.1f}ms)print(f加速:{t_random/t_contiguous:.2f}x)# 实测# 随机访问: 156ms# 连续访问: 89ms# 快了 1.75x优化手段4预取让计算和数据加载并行# 不好串行执行forbatchindataloader:databatch.to(npu)# CPU→NPU数据传输阻塞outputmodel(data)# 计算等待数据传输完才能开始# 好用prefetch重叠数据传输和计算fromtorch.utils.dataimportDataLoaderfromtorch.multiprocessingimportProcess,Queuedefdata_prefetch(queue,dataloader):单独进程做数据预取forbatchindataloader:batch_npubatch.to(npu,non_blockingTrue)# 异步传输queue.put(batch_npu)deftraining_loop_with_prefetch():queueQueue(maxsize2)# 缓冲2个batch# 启动预取进程pProcess(targetdata_prefetch,args(queue,dataloader))p.start()forbatchinrange(num_batches):# 从队列拿数据已经传输好了dataqueue.get()# 计算和下一个batch的预取并行outputmodel(data)# 当前batch计算# 此时data_prefetch进程已经在加载下一个batch了p.terminate()# 更简单的方式用DataLoader的prefetch_factordataloaderDataLoader(dataset,batch_size32,num_workers8,# 多进程加载pin_memoryTrue,# 页锁定内存加速传输prefetch_factor2,# 每个worker预取2个batchpersistent_workersTrue)# prefetch_factor2的意思# 当GPU正在处理batch i时worker已经在准备batch i1和batch i2了优化手段5混合精度减少内存访问量fp16的数据量是fp32的一半HBM带宽压力直接减半。# fp32每个元素4字节x_fp32torch.randn(1,64,4096,4096).npu()# 64GB/s带宽压力# fp16每个元素2字节x_fp16x_fp32.half()# 数据量减半带宽压力减半# bf16也是2字节但精度比fp16好x_bf16x_fp32.to(torch.bfloat16)# 混合精度的正确用法fromtorch.npu.ampimportautocast modelmodel.half()# 模型转fp16withautocast():# 自动选择哪些层用fp16哪些用fp32outputmodel(input)# 中间计算用fp16losscriterion(output,target)# loss可能需要fp32精度# 注意loss计算用fp32防止累加误差lossloss.float()# 确保loss是fp32优化手段6梯度检查点以带宽换容量显存不够的时候会用梯度检查点checkpointing但它也有带宽优化的效果——用计算换带宽。# 梯度检查点的原理# 不保存所有中间激活只保存每层的输入# 反向传播时重新算中间激活# PyTorch实现fromtorch.utils.checkpointimportcheckpoint_sequentialclassMyModel(nn.Module):def__init__(self,layers):super().__init__()self.layersnn.ModuleList(layers)defforward(self,x):# 不用checkpoint所有中间结果都存HBM# return self.layers(x)# 用checkpoint只存每层的输入中间结果重算returncheckpoint_sequential(self.layers,x)# 带宽对比# 不用checkpointforward时写激活4GBbackward时读激活4GB 8GB HBM访问# 用checkpointforward不存激活backward重算额外2GB计算 4GB HBM访问 2GB重算# 性能# 带宽节省50%# 计算增加约20%中间激活要重算# 适用于带宽瓶颈 计算相对空闲的场景带宽优化的检查清单# 按这个顺序排查带宽问题# 1. 先确认是不是带宽瓶颈hbm_bwget_hbm_bandwidth()ifhbm_bw70:print(→ 不是带宽瓶颈是其他问题计算/通信)exit()# 2. 找到带宽占用最大的算子top_opsget_top_bandwidth_ops(10)foropintop_ops:print(f{op.name}:{op.bandwidth_gb}s ({op.percentage}%))# 3. 针对最大的算子逐个优化foropintop_ops:ifConvinop.name:# 检查是否融合了BatchNormifnotis_fused_with_bn(op):print(f→{op.name}没有融合BN建议融合)elifMatMulinop.name:# 检查是否用了正确的tilingifnothas_optimal_tiling(op):print(f→{op.name}的tiling不是最优建议优化)
http://www.gsyq.cn/news/1352358.html

相关文章:

  • Python __slots__ 入门指南
  • 基于魔珐星云打造的办公室助理数字人:高效办公、智能协作、语音随时交互
  • 回测年化50%,实盘亏20%:99%量化新手都会犯的7个致命错误
  • 让ClaudeCode成本爆降89%,这个开源工具有点猛...
  • Spring Boot 集成阿里云 OSS 实现文件上传下载的完整指南(从概念到代码)
  • 用 PS 抠公章最详细步骤|零基础一键抠取透明公章
  • 解锁Linux无线网卡配置:RTL8821CU驱动实战深度指南
  • 量子纠错码与逻辑门优化实现技术解析
  • Keil µVision TAB显示异常问题分析与解决方案
  • A51汇编器Error 21解析与8051开发实践
  • 量子计算与人工智能融合:技术原理与应用前景
  • Cortex-M3/M4处理器模式判断与调试技巧
  • 量子退火与模拟退火在组合优化中的应用对比
  • RIS辅助MA系统的近场DM设计与优化
  • 好莱坞已悄悄启用AI拍片:2024年7部奥斯卡入围作品背后的生成式视频技术全拆解
  • 基于人工神经网络的船舶配员人数预测模型
  • AI安全简报与模型能力发布机制解析
  • 本地化PentestGPT实战指南:Llama 3-70B红队渗透五步工作流
  • ViT-G大模型引发GPU掉线的硬件级故障诊断与规避
  • Node.js crypto模块跨版本兼容性解决方案
  • Android签名校验绕过实战:Frida动态Hook四层防御体系
  • 量子态相似性度量:迹距离与保真度的工程应用
  • 联想集团第一季营收216亿美元:净利5.9亿美元 股价上涨19% 市值近2000亿港元
  • 开源大模型本地部署与轻量化实践指南
  • PHP逆向工程实战:HTTP黑盒调试、SDK解混淆与Swoole协程故障定位
  • Claude Mythos Preview:AI主导攻防的范式跃迁
  • GE图引擎架构剖析:怎么做到“代码零修改,性能最大化“
  • Frida内存提取实战:Android so与dex动态dump技术详解
  • Unity中大型项目架构选型:GameFramework与QFramework实战对比
  • 蛋白质基础模型:从AlphaFold2到Chai-1的范式跃迁