利用NsightSystem分析动转静性能
利用NsightSystem分析动转静性能
前置知识
NsightSystem 是一个集终端 CUDA Profile 日志生成和 前端可视化 timeline 分析的强大工具。安装 nsys 需要分别下载适合 Unix 的 Installer 和 Mac/Windows 的可视化终端。
背景
在部分场景下,开启SOT后的性能劣于动态图,主要的可行优化点在于:
- 减少子图打断
支持动态shape,numpy()等
- 热启动优化
Guard等机制会随着模型的变大而越来越大,越来越慢
- 冷启动优化
提高cache命中率、减少编译时间(前者可以促成后者)
Windows端安装NSystem
在GUI当中查看profile,在前置知识提到的链接中进行下载。
下载带GUI的Full Version
用.msi文件直接安装
Linux端安装NSystem
安装不带GUI的版本,我们只会在Windows或者mac端进行可视化分析。
下载.deb包后,用命令安装
(py310) λ yq01-sys-hic-k8s-v100-qm-a223-0007 /workspace dpkg -i NsightSystems-linux-cli-public-2025.1.1.103-3542797.deb
输出如下,则安装成功
(py310) λ yq01-sys-hic-k8s-v100-qm-a223-0007 /workspace dpkg -i NsightSystems-linux-cli-public-2025.1.1.103-3542797.deb
Selecting previously unselected package nsight-systems-cli-2025.1.1.
(Reading database ... 32179 files and directories currently installed.)
Preparing to unpack NsightSystems-linux-cli-public-2025.1.1.103-3542797.deb ...
Unpacking nsight-systems-cli-2025.1.1 (2025.1.1.103-251135427971v0) ...
Setting up nsight-systems-cli-2025.1.1 (2025.1.1.103-251135427971v0) ...
update-alternatives: using /opt/nvidia/nsight-systems-cli/2025.1.1/target-linux-x64/nsys to provide /usr/local/bin/nsys (nsys) in auto mode
最后update-alternatives: ...
信息的意思是(来自GPT):
这条消息是 update-alternatives
机制的输出,表示系统已经自动为 nsys
命令设置了一个全局可用的路径。
解析这条消息
update-alternatives: using /opt/nvidia/nsight-systems-cli/2025.1.1/target-linux-x64/nsys
to provide /usr/local/bin/nsys (nsys) in auto mode
意思是:
update-alternatives
是一个 Linux 系统管理多个软件版本的工具,它用于在不同的可执行文件之间创建符号链接(symlink)。- 这里,它把
/opt/nvidia/nsight-systems-cli/2025.1.1/target-linux-x64/nsys
设置为/usr/local/bin/nsys
的默认执行文件。 auto mode
表示系统自动选择了这个nsys
作为默认选项(而不是用户手动指定)。
如何查看和管理 nsys
的 alternatives
你可以用以下命令检查 nsys
的 alternatives 设置:
update-alternatives --display nsys
如果你想手动选择不同的 nsys
版本(如果你安装了多个版本),可以运行:
sudo update-alternatives --config nsys
它会列出所有可用的 nsys
版本,你可以选择一个。
这对你意味着什么?
- 你现在可以直接在终端输入
nsys
来使用 Nsight Systems CLI,无需手动指定完整路径。 nsys
实际上指向/opt/nvidia/nsight-systems-cli/2025.1.1/target-linux-x64/nsys
,但系统已经帮你自动处理了路径问题。
如果 nsys --version
能正确返回信息,那说明安装已经完成,你可以正常使用 nsys
了!🚀
NSys试运行
实验代码
用一个简单的伪推理程序来检查nsys
是否能够正常执行:
import os
import unittest
import numpy
# os.environ["FLAGS_prim_all"] = "true"
# os.environ["FLAGS_prim_enable_dynamic"] = "true"
# os.environ["FLAGS_use_cinn"] = "1"
import paddle
build_strategy = paddle.static.BuildStrategy()
# build_strategy.build_cinn_pass = True
S2 = 32
def init():
var_kwarg_middle_1 = paddle.rand([64, 26, 512, S2, 40])
var_kwarg_middle_1 = paddle.cast(var_kwarg_middle_1, "float16")
var_kwarg_middle_2 = paddle.rand([64, 1, 512, 1, 40])
var_kwarg_middle_2 = paddle.cast(var_kwarg_middle_2, "float16")
var_23 = paddle.rand([64, 26, 512])
var_kwarg_middle_3 = paddle.rand([64, 26, 512, S2, 40])
var_kwarg_middle_3 = paddle.cast(var_kwarg_middle_3, "float16")
return var_kwarg_middle_1, var_kwarg_middle_2, var_23, var_kwarg_middle_3
def func(var_kwarg_middle_1, var_kwarg_middle_2, var_23, var_kwarg_middle_3):
var_28 = paddle.cast(var_23, dtype="float16")
# var_29 = paddle.to_tensor([64, 26, 512, 1, 1])
# var_30 = paddle.reshape(var_28, var_29)
var_30 = paddle.reshape(var_28, [64, 26, 512, 1, 1])
# var_31 = paddle.to_tensor([64, 26, 512, 32, 40])
# var_32 = paddle.expand(var_30, var_31)
# var_33 = paddle.to_tensor([64, 26, 512, 32, 40])
# var_34 = paddle.expand(var_kwarg_middle_1, var_33)
# var_35 = var_32 * var_34
var_35 = var_30 * var_kwarg_middle_1
var_36 = paddle.sum(var_35, keepdim=True, axis=[1, 3])
# var_37 = paddle.to_tensor([64, 26, 512, 32, 40])
# var_38 = paddle.expand(var_kwarg_middle_2, var_37)
# var_39 = var_32 * var_38
var_39 = var_30 * var_kwarg_middle_2
var_40 = var_39
var_41 = paddle.reshape(var_36, [64, 512, 1, 40])
var_42 = var_41
return var_40, var_42
# x.reshape([64, 26, 512, paddle.shape(kwarg_middle_3)[3], 40])
def input_spec():
return [
paddle.static.InputSpec(
# shape=[None, None, None, None, None],
shape=[64, 26, 512, None, 40],
dtype="float16",
name="var_kwarg_middle_1",
),
paddle.static.InputSpec(
shape=[64, 1, 512, 1, 40],
dtype="float16",
name="var_kwarg_middle_2",
),
paddle.static.InputSpec(
shape=[64, None, 512], dtype="float32", name="var_23"
),
paddle.static.InputSpec(
shape=[64, 26, 512, None, 40],
dtype="float16",
name="var_kwarg_middle_3",
),
]
class TestCase(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def compare_result(self, dy_compute, data_init):
static_compute = paddle.jit.to_static(
full_graph=True,
build_strategy=build_strategy,
input_spec=input_spec(),
)(dy_compute)
inputs = data_init()
dy_out = dy_compute(*inputs)
for step in range(100):
with paddle.profiler.utils._nvprof_range(step, 50, 60):
st_out = static_compute(*inputs)
if isinstance(dy_out, paddle.Tensor):
numpy.testing.assert_allclose(dy_out, st_out, atol=1e-3, rtol=1e-3)
return
for d, s in zip(dy_out, st_out):
numpy.testing.assert_allclose(d, s, atol=1e-3, rtol=1e-3)
def test_case(self):
self.compare_result(func, init)
if __name__ == "__main__":
unittest.main()
实验结果
运行如下命令
(py310) λ yq01-sys-hic-k8s-v100-qm-a223-0007 /workspace/Paddle/xym/py_dir CUDA_VISIBLE_DEVICES=3 nsys profile -t cuda,nvtx --cpuctxsw=process-tree -o nsys-test --capture-range=cudaProfilerApi --force-overwrite true python nsys_test.py
输出信息如下:
WARNING: Device-side CUDA Event completion trace is currently enabled.
This may increase runtime overhead and the likelihood of false
dependencies across CUDA Streams. If you wish to avoid this, please
disable the feature with --cuda-event-trace=false.
grep: warning: GREP_OPTIONS is deprecated; please use an alias or script
W0208 08:40:24.016208 20209 gpu_resources.cc:119] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 12.0, Runtime API Version: 12.3
W0208 08:40:24.017017 20209 gpu_resources.cc:164] device: 0, cuDNN Version: 9.0.
W0208 08:40:24.017033 20209 gpu_resources.cc:196] WARNING: device: 0. The installed Paddle is compiled with CUDA 12.3, but CUDA runtime version in your machine is 12.0, which may cause serious incompatible bug. Please recompile or reinstall Paddle with compatible CUDA version.
I0208 08:40:24.116797 20209 pir_interpreter.cc:1604] New Executor is Running ...
I0208 08:40:24.117646 20209 pir_interpreter.cc:1628] pir interpreter is running by multi-thread mode ...
Capture range started in the application.
Capture range ended in the application.
Generating '/tmp/nsys-report-756f.qdstrm'
--------------------------------------
C++ Traceback (most recent call last):
--------------------------------------
0 paddle::platform::CudaProfilerStop()
----------------------
Error Message Summary:
----------------------
FatalError: `Termination signal` is detected by the operating system.
[TimeInfo: *** Aborted at 1739004027 (unix time) try "date -d @1739004027" if you are using GNU date ***]
[SignalInfo: *** SIGTERM (@0x4ee5) received by PID 20209 (TID 0x7f8272f17780) from PID 20197 ***]
[1/1] [0% ] nsys-test.nsys-repProcessing events...
[1/1] [========================100%] nsys-test.nsys-rep
Generated:
/workspace/Paddle/xym/py_dir/nsys-test.nsys-rep
虽然输出了Fatal Error
,但是可以看到这里并不是段错误❌,而是SIGTERM
,这个信号是在监控装饰器的结尾程序主动发出的,我们这里只监控50-60个循环,超出60个循环,就发出结束信号了。
学习NSys GUI的使用
准备工作
发现了一个一直没有注意的warning
,但好像问题不大
W0210 10:28:10.189069 10936 gpu_resources.cc:196] WARNING: device: 0. The installed Paddle is compiled with CUDA 12.3, but CUDA runtime version in your machine is 12.0, which may cause serious incompatible bug. Please recompile or reinstall Paddle with compatible CUDA version.
但是为了控制变量,保证不是其他地方出错,把这个问题也解决了。
(base) λ yq01-sys-hic-k8s-v100-qm-a223-0007 /workspace/performance-analysis/PaddleYOLO ls /usr/local/ | grep cuda
grep: warning: GREP_OPTIONS is deprecated; please use an alias or script
cuda
cuda-12
cuda-12.3/
检查发现,这个机子上,既有12.0的run time,也有12.3的,那么我们只要把12.3加到LD_LIBRARY_PATH
的前面,就能保证优先使用12.3的了。
export LD_LIBRARY_PATH=/usr/local/cuda-12.3/lib64:$LD_LIBRARY_PATH
subgraph_relation
读法
这里箭头上写的-1
表示这个函数输出的是动态shape,而因为此处输入已经确定了,所以输出也是确定的,因此输出向量不是-1,而是一个具体的shape
模型1:PaddleYOLO_yolov5_l_300e_coco_bs16_fp32_DP
在不改变配置文件的情况下,动态图可以跑到结束,但是动态图跑到20多轮就会出问题,一个是:
Traceback (most recent call last):
File "/workspace/performance-analysis/PaddleYOLO/tools/train.py", line 202, in <module>
main()
File "/workspace/performance-analysis/PaddleYOLO/tools/train.py", line 198, in main
run(FLAGS, cfg)
File "/workspace/performance-analysis/PaddleYOLO/tools/train.py", line 151, in run
trainer.train(FLAGS.eval)
File "/workspace/performance-analysis/PaddleYOLO/ppdet/engine/trainer.py", line 513, in train
profiler.add_profiler_step(profiler_options)
File "/workspace/performance-analysis/PaddleYOLO/ppdet/utils/profiler.py", line 103, in add_profiler_step
paddle.utils.profiler.start_profiler(_profiler_options['state'],
AttributeError: module 'paddle.utils' has no attribute 'profiler'
虽然这个不是直接导致程序崩溃的问题,但还是把相应的代码注释了。
此处代码不影响正确性,是用来监控的,所以可以放心去掉。
另一处是跟dataloader
相关的一个报错,是致命问题,会导致程序退出,在配置文件configs/yolov5/_base_/yolov5_reader_high_aug.yml
中,把这一行
use_shared_memory: True
更改为false后,程序不再崩溃,但是ips
非常低;动态图亦是如此,原因有待进一步分析。
模型2:PaddleYOLO_yolov5_l_300e_coco_bs16_fp32_DP
估计所有模型都存在profiler
相关的问题,因为那部分代码已经注释,所以不会再产生影响,故不再赘述。
插曲
测的过程中遇到了一个小插曲:
一直在爆显存
Out of memory error on GPU 0. Cannot allocate 92.250000MB memory on GPU 0, 15.716553GB memory has been allocated and available memory is only 57.125000MB.
Please check whether there is any other process using GPU 0.
1. If yes, please stop them, or start PaddlePaddle on another GPU.
2. If no, please decrease the batch size of your model.
(at /workspace/Paddle/paddle/phi/core/memory/allocation/cuda_allocator.cc:71)
这里显示的是device 0
,我还以为是这个进程跑到GPU 0
上去了,看了半天,每次跑都是0,最后才发现这里的0意思是,该进程可见的显卡里面的0号,因为我设置了CUDA_VISIBLE_DEVICES=7
,所以它只能看见7号卡,这里只会输出0;
要验证进程跑到了哪个卡上,可以watch
一下nvidia-smi
,看看哪张卡的显存占用一直在往上涨。
至此解决办法也就出来了:减少batch_size.
插曲2
注意,我们直接运行的命令是
bash test_tipc/benchmark_train.sh test_tipc/configs/yolov5/yolov5_l_300e_coco_train_infer_python.txt benchmark_train dynamicTostatic_bs16_fp32_DP_N1C1
但真正影响训练中batch_size的配置不在这个txt文件里,而是在这里:
# run test_train_inference_python.sh
cmd="bash test_tipc/test_train_inference_python.sh ${FILENAME} benchmark_train > ${log_path}/${log_name} 2>&1 "
echo $cmd
eval $cmd
eval "cat ${log_path}/${log_name}"
这个bash文件也会引用一个txt配置文件,那里面的batch_size
才是训练过程中实际生效的。
真正生效的配置如下(部分节选):
===========================train_params===========================
model_name:yolov5_l_300e_coco
python:python
gpu_list:7
use_gpu:True
auto_cast:fp32
epoch:benchmark_train=1
save_dir:null
TrainReader.batch_size:benchmark_train=16
pretrain_weights:https://paddledet.bj.bcebos.com/models/yolov5_l_300e_coco.pdparams
trained_model_name:model_final.pdparams
train_infer_img_dir:./dataset/coco/test2017/
--profiler_options:null
有效的部分就是这个TrainReader.batch_size:benchmark_train
,尝试修改为8,但是一运行bash
脚本,又立刻被修改为8了,通过二分法定位到是在test_tipc/benchmark_train.sh
文件中发生的修改操作,定位到原因如下:
我们默认使用的命令是
bash test_tipc/benchmark_train.sh test_tipc/configs/yolov5/yolov5_l_300e_coco_train_infer_python.txt benchmark_train dynamicTostatic_bs16_fp32_DP_N1C1
benchmark_train.sh
会分析我们的参数,然后把bs16
填入到配置文件,所以我们只要在这里修改就行,不用修改配置文件本身。
问题1
NSys
打出来的日志十分抽象;在SOT执行期,负载莫名其妙地从一个线程转移到了另一个线程;而且在前向和后向之间有比较大的一块空白没有任何event
被记录,非常的玄学。
可行的解决办法:
- 进一步降低
batch_size
,看看是不是显存导致的问题。 - 在代码中增加更多的日志输出(即在Nsys报告中,空白前的那些函数,看看他们后续都执行了什么)。
- 看看Allacator后面都执行了什么。
- 可以开
GLOG_v=7
,这里的GLOG
是GoogleLogging
- 看看
FallbackWrapper
后面都执行了什么。
问题2
有一个规模比较大的子图,一直在重新编译,总共输出了30张subgraph_relation
,后来发现是一个Guard Error
导致的,已经有相应的修复PR,可以后续观察一下合入后的效果,说不定能顺带解决问题1。
修复手段1
逐步降低batch_size
发现,在这个过程中动态图和动转静的ips
都会提升,但是动转静性能始终不及动态图,ips
差了10左右.
修复手段2
合入了修复numpy
相关问题的PR之后,再次进行测试,发现ips
指标如下:
BS4
动态图:
24.6234
动转静:
Average ips: 19.9879
BS8
动态图:
Avg: 27.645 images/s
动转静:
Avg: 17.862 images/s
可以看到,依然比动态图慢,而且诡异的是,在动转静模式下,提高batch_size
之后,ips
反而下降了。
小进展
subgraph
的数量从原来的30个下降到了2个,原来重复编译的问题确实被成功解决了。
观察此时的新nsys-repo
发现:
在动转静执行流程中,有一个set_value_dygraph
方法被大量重复调用,它的位置处在一个FallbackWrapper
的后面,而这个FallbackWrapper
正是我们封装之后的SIR
执行器,因此,我们能够推断出:
这个重复的函数调用,发生在动态图的执行路径上。
因为问题出在动态图执行路径,所以我们肯定要在日志中找FallbackError
出现的位置。
修复手段3
因为根据修复手段2
当中的信息,我们能够确认问题出在动态图执行路径上,因此我们在日志中搜索FallbackError
,发现两处均来自同一段模型源代码yolo_v5.py
:
def yolov5_loss(self, pi, t_cls, t_box, t_indices, t_anchor, balance):
loss = dict()
b, a, gj, gi = t_indices # image, anchor, gridy, gridx
n = b.shape[0] # number of targets
tobj = paddle.zeros_like(pi[:, :, :, :, 4])
loss_box = paddle.to_tensor([0.])
loss_cls = paddle.to_tensor([0.])
if n:
mask = paddle.stack([b, a, gj, gi], 1)
ps = pi.gather_nd(mask)
# Regression
pxy = F.sigmoid(ps[:, :2]) * 2 - 0.5
pwh = (F.sigmoid(ps[:, 2:4]) * 2)**2 * t_anchor
pbox = paddle.concat((pxy, pwh), 1)
iou = bbox_iou(pbox.T, t_box.T, x1y1x2y2=False, ciou=True)
loss_box = (1.0 - iou).mean()
# Objectness
score_iou = paddle.cast(iou.detach().clip(0), tobj.dtype)
# with paddle.no_grad():
# x = paddle.gather_nd(tobj, mask)
# tobj = paddle.scatter_nd_add(
# tobj, mask, (1.0 - self.gr) + self.gr * score_iou - x)
with paddle.no_grad():
tobj[b, a, gj, gi] = (1.0 - self.gr
) + self.gr * score_iou # iou ratio
# Classification
if self.num_classes > 1: # cls loss (only if multiple classes)
# t = paddle.full_like(ps[:, 5:], self.cls_neg_label)
# t[range(n), t_cls] = self.cls_pos_label
# loss_cls = self.BCEcls(ps[:, 5:], t)
t = paddle.full_like(ps[:, 5:], self.cls_neg_label)
if not self.to_static:
t = paddle.put_along_axis(
t,
t_cls.unsqueeze(-1),
values=self.cls_pos_label,
axis=1)
else:
for i in range(n):
t[i, t_cls[i]] = self.cls_pos_label
loss_cls = self.BCEcls(ps[:, 5:], t)
obji = self.BCEobj(pi[:, :, :, :, 4], tobj) # [bs, 3, h, w]
loss_obj = obji * balance
loss['loss_box'] = loss_box * self.loss_weights['box']
loss['loss_obj'] = loss_obj * self.loss_weights['obj']
loss['loss_cls'] = loss_cls * self.loss_weights['cls']
return loss
这里用到了装饰器paddle.no_grad()
,导致整个函数都被Fallback
掉了,因此我们可以把被装饰器的部分单独拿出来,封装成一个函数,这样的话就只有被装饰的部分代码会引起Fallback
手段3效果
修改过后,从subgraph_relation
当中能够看出,子图数量确实变多了,说明这里从Fallback
变成了打断,但是性能依然没有赶上动态图,对比之前的SOT表现,ips
只提升了不到1,几乎可以当做是误差。
另一方面,原来修改前只有一种FallbackError
,全都是不支持字节码SETUP_WITH
导致的,现在因为进入静态图执行流程的代码变多,出现了另一种新的FallbackError
:
paddle.jit.sot.utils.exceptions.FallbackError: Currently don't support predicate SymbolicVariable
重新观察Nsys
的报告,发现性能瓶颈在SIR_19
的后面:
而SIR_19
对应的函数是bbox_utils.poy
当中的bbox_iou
。
修复手段4
发现这些性能瓶颈是大量重复的小event,可以确认他们来自于一个循环,再检查一下update_tobj
后面的代码
self.update_tobj(tobj, score_iou, b, a, gj, gi)
# Classification
if self.num_classes > 1: # cls loss (only if multiple classes)
# t = paddle.full_like(ps[:, 5:], self.cls_neg_label)
# t[range(n), t_cls] = self.cls_pos_label
# loss_cls = self.BCEcls(ps[:, 5:], t)
t = paddle.full_like(ps[:, 5:], self.cls_neg_label)
if not self.to_static:
t = paddle.put_along_axis(
t,
t_cls.unsqueeze(-1),
values=self.cls_pos_label,
axis=1)
else:
for i in range(n):
t[i, t_cls[i]] = self.cls_pos_label
这里已经完美符合条件了,动静执行路径不同,且在SOT
的执行路径上有一个循环,直接把SOT
的执行路径替换为和动态图一致:
# Classification
if self.num_classes > 1: # cls loss (only if multiple classes)
# t = paddle.full_like(ps[:, 5:], self.cls_neg_label)
# t[range(n), t_cls] = self.cls_pos_label
# loss_cls = self.BCEcls(ps[:, 5:], t)
t = paddle.full_like(ps[:, 5:], self.cls_neg_label)
# if not self.to_static:
if True:
t = paddle.put_along_axis(
t,
t_cls.unsqueeze(-1),
values=self.cls_pos_label,
axis=1)
# else:
# for i in range(n):
# t[i, t_cls[i]] = self.cls_pos_label
# print(f"n: {n} i: {i}, t_cls: {t_cls[i]}, cls_pos_lable: {self.cls_pos_label}\n")
接下来只要确认精度没问题就行了。
修复成果
修改后重新进行测试,数据如下(已固定随机数种子):
动转静:
Extract 1500 records: separator=None; position=None
average ips of 1500 steps, skip 0 step:
Avg: 27.021 images/s
FPS: 27.021 images/s
average ips of 1500 steps, skip 4 steps, valid steps 1348 :
var: 1.085
Avg: 27.276 images/s
Min: 23.668 images/s
Max: 28.070 images/s
diff_min_max: -15.682 %
FPS: 27.276 images/s
Loss: 0.251002
动态图:
Extract 1695 records: separator=None; position=None
average ips of 1695 steps, skip 0 step:
Avg: 27.817 images/s
FPS: 27.817 images/s
average ips of 1695 steps, skip 4 steps, valid steps 1523 :
var: 2.692
Avg: 28.109 images/s
Min: 22.525 images/s
Max: 29.142 images/s
diff_min_max: -22.707 %
FPS: 28.109 images/s
Loss: 0.174485
ips和loss还是存在一定差距,但是已经非常接近了。
经过努力后,loss还是没有和动态图对齐,现在暂定目标如下:
只要这处修改不影响loss,即SOT本身在修改前后的loss能够对齐,那么我们就认为问题解决,可以提PR了。
模型2:PaddleVideo_VideoSwin_bs1_fp32_DP
本文作者:Gold_stein
本文链接:https://www.cnblogs.com/smartljy/p/18703086
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步