cuBlas API Launch Latency 耗时异常分析记录
一、背景
最近在做 AI 编译器生成 Kernel 支持 Bert 模型训练调优工作,在分析 bert 的timeline中发现,在每个 step 的前两个 cinn_instruction_run 之后,总是固定跟着一个 2.5 ms 左右的空白。但 HOST 端其实很早就 emit 了CUDA API,只是为什么 GPU 要有那个大的 Latency 后才执行呢?
从 Nvidia 官方论坛上可知,正常情况下一个 cuda kernel launch 的 Latency 在 us 级别。
- Nvidia 官方文档原文:CUDA kernel launch latency could be defined as the time range from the beginning of the launch API call to the beginning of the kernel execution. There are about 20 µs of launch latency. If the launch API call takes 10 µs on your system, you can only launch at most 100,000 kernels per second.
- Nvidia 官方论坛讨论:Kernel launch overhead is frequently cited as 5 microseconds. My understanding of the PCIe transactions is limited, but best I know a kernel launch requires at least two transactions: (1) host sending a kernel launch command to the GPU (2) GPU sending an acknowledgement back to the host.

二、研习 Nvidia 手册
2.1 官网手册
在Nvidia的官方文档中,Overhead 主要包括如下几个部分:
- CPU wrapper
- memory
- GPU lauch overhead
2.2 CPU wrapper
这部分要包含了在多线程下硬件上所有的 mutex-lock 相关的操作。若进行了 mutex 相关操作,在 Nsight Timeline 的 os runtime 那一行会出现 pthread_mutex_lock
原文: This includes any mutex-lock contention that occurs in the driver if doing multi-threaded launching. You can see if you are hitting mutex contention within the driver by collecting OS Runtime data, which shows any pthread_mutex_lock calls lasting above a user-settable threshold.

2.3 memory
这部分主要包括数据搬运的开销,如 H2D、D2H、D2D。
原文:This is the overhead of moving data back and forth from the CPU to the GPU, or from one GPU to another. For example, this would be the time it takes to read the input tensors and writing output to DRAM.
2.4 GPU launch overhead
这部分主要包括从「取一个command」到「GPU上开始执行」之间的时间开销。主要包括:
原文:This is the time it takes for the GPU to retrieve the command and begin executing it.
- GPU 上可能有不同的
context active,在执行一个新的应用程序时,需要进行「上下文」切换,比如 GPU 正在渲染 PC Desktop,则需要进行上下文切换以运行另一个 command 任务。若命中「上下文切换」,这会通过收集GPU Context Switch信息展示出来。(绿色表示没有进行切换) - GPU 可能被前面的 comman 给 blocked 阻塞住,触发等待操作
- CUDA 支持多 stream,且每个 stream 的 kernel 是序贯执行的,且 memcpys 必须按照顺序执行。
- GPU 必须按照优先级,优先执行优先级高的 kernel

这里从Nvidia的官方文档上,发现了一个很重要的信息:Nsight 会额外实时收集CPU 上的 IP/Backstrace 信息,就是上图的中黄色方框的sampling point,这个可以辅助判断当前时间节点 CPU 在做什么事情。
原文:Sampling data was also collected, as you can see by the orange/yellow marks below the thread state timeline. Each mark represents the point when a CPU IP/backtrace sample was collected. When this screenshot was captured, the mouse (not shown) was hovering on the sampling mark just above the left side of the tooltip. The tooltip shows the CPU IP/backtrace for that thread at that moment. Looking at the vectorAdd source code, you can easily see the application was checking the results of the GPU’s calculation.
三、 GLOG_v日志和源码
首先看下空白后面这个 Kernel 的代码:
function fn_broadcast_to_224_elementwise_add_225_reshape_264_transpose_303_1614_kernel (_linear_2__b_0, _var_1137, _var_1381)
if ((blockIdx.x < 12288)) {
if ((threadIdx.x < 1024)) {
var_1381[((1024 * blockIdx.x) + threadIdx.x)] = (var_1137[((((blockIdx.x % 96) / 8) * 64) + ((768 * (threadIdx.x / 64)) + (((blockIdx.x / 96) * 98304) + ((12288 * (blockIdx.x % 8)) + (threadIdx.x % 64)))))] + linear_2__b_0[(((threadIdx.x % 64) + (((blockIdx.x % 96) / 8) * 64)) % 768)])
}
}
但是,通过分析不同 step 的初始空白,发现有不同的情况。在 Bert 模型训练中,前面的几个 Kernel 对应3个平行的 matmul + fn_broadcast_to_elementwise_add_reshape_transpose 组合对,我们下面简称为:matmul + fn_xx 吧:
- 场景一:
fn_xx之后出现大空白 - 场景二:
matmul之后出现大空白
由此可知,大空白的出现与 Kernel 不是强烈耦合的,可能有其他潜在的原因在里面,因为我们首先要找到「是什么因素影响了这个Latency」?

总览的看了不同step的timeline,发现不同step下的GPU 空白表现不稳定,有的step下GPU占用率会比较好,有的step下GPU空白会比较多,如下图:

四、新A100机器上交叉复测
详细分析了A100 机器 A 上的timeline,违背经验认知,故在之前分布式队列上下线的A100 机器 B 上安装 NSight 脚本交叉复测一组 Timeline 文件,排除机器的影响(A 是一个多人复用的开发机,B 机器使用的人比较少)
从 timeline上可以看出,在新的 A100 机器上,情况就比较简单了:稳定在第一个matmul 的 cublas API 之后,且 HgemmStridedBatched 的开销与空白时间严格对应。这一点跟 A机器上完全不同,从文档的最前面 timeline 来看,HgemmStridedBatched 的API 调用位置是与GPU stream的时间点是错开的。

也许这个 HgemmStridedBatched 是一个可以深入分析的思路。首先我们先对比下与原生动转静的 timeline 里的 Kernel,从下图可以看处,主要包括两大类:
- GemmEx
- GemmStridedBatchedEx

CINN 里统一使用的 HgemmStridedBatched 查看了相关API调用的入口函数源码,在函数各个分支里添加了VLOG,输出必要的信息,查看走了哪些分支:
void cinn_call_cublas(void *v_args,
int num_args,
bool trans_a,
bool trans_b,
bool trans_o,
float alpha,
float beta,
int a1,
int a2,
int a3,
int a4,
int b1,
int b2,
int b3,
int b4,
void *stream) {
// 省略
CUBLAS_CALL(cublasGemmStridedBatched(cuda_dtype,
cuhandle,
trans_op_l,
trans_op_r,
m,
n,
k,
alpha,
lhs,
ldl,
stride_l,
rhs,
ldr,
stride_r,
beta,
C,
ldc,
m * n,
batch));
// 省略
}
场景一:[128, 128, 768] * [768, 768] ,调用了 cublasGemmStridedBatched ,但主框架是调用的是 GemmEX ,不太符合预期
I0508 09:34:19.758335 99667 cuda_util.cc:134] a1: 1
I0508 09:34:19.758383 99667 cuda_util.cc:135] a2: 128
I0508 09:34:19.758399 99667 cuda_util.cc:136] a3: 128
I0508 09:34:19.758404 99667 cuda_util.cc:137] a4: 768
I0508 09:34:19.758407 99667 cuda_util.cc:138] b1: 1
I0508 09:34:19.758412 99667 cuda_util.cc:139] b2: 1
I0508 09:34:19.758419 99667 cuda_util.cc:140] b3: 768
I0508 09:34:19.758422 99667 cuda_util.cc:141] b4: 768
I0508 09:34:19.758430 99667 cuda_util.cc:183] call cublasGemmStridedBatched with batch 128, isl: 0 isr: 98304

场景二:[128, 12, 128, 64] * [128, 12, 128 ,64] ,shape相同,trans_b = True ,符合预期。
I0508 09:47:09.494791 100378 cuda_util.cc:134] a1: 128
I0508 09:47:09.494797 100378 cuda_util.cc:135] a2: 12
I0508 09:47:09.494799 100378 cuda_util.cc:136] a3: 128
I0508 09:47:09.494801 100378 cuda_util.cc:137] a4: 64
I0508 09:47:09.494804 100378 cuda_util.cc:138] b1: 128
I0508 09:47:09.494807 100378 cuda_util.cc:139] b2: 12
I0508 09:47:09.494809 100378 cuda_util.cc:140] b3: 128
I0508 09:47:09.494812 100378 cuda_util.cc:141] b4: 64
I0508 09:47:09.494813 100378 cuda_util.cc:142] trans_a: 0
I0508 09:47:09.494817 100378 cuda_util.cc:143] trans_b: 1
I0508 09:47:09.494818 100378 cuda_util.cc:144] trans_o: 0
I0508 09:47:09.494822 100378 cuda_util.cc:217] call cublasGemmStridedBatched sl: 8192 sr: 8192
场景三:[128, 12, 128, 128] * [128, 12, 128, 64] ,符合预期
I0508 09:47:09.495852 100378 cuda_util.cc:134] a1: 128
I0508 09:47:09.495857 100378 cuda_util.cc:135] a2: 12
I0508 09:47:09.495860 100378 cuda_util.cc:136] a3: 128
I0508 09:47:09.495862 100378 cuda_util.cc:137] a4: 128
I0508 09:47:09.495865 100378 cuda_util.cc:138] b1: 128
I0508 09:47:09.495867 100378 cuda_util.cc:139] b2: 12
I0508 09:47:09.495870 100378 cuda_util.cc:140] b3: 128
I0508 09:47:09.495872 100378 cuda_util.cc:141] b4: 64
I0508 09:47:09.495874 100378 cuda_util.cc:142] trans_a: 0
I0508 09:47:09.495877 100378 cuda_util.cc:143] trans_b: 0
I0508 09:47:09.495879 100378 cuda_util.cc:144] trans_o: 0
I0508 09:47:09.495882 100378 cuda_util.cc:217] call cublasGemmStridedBatched sl: 8192 sr: 16384
五、优化思路
5.1 最小复现样例
根据 Bert 里的模型结构抽离了最小代码case:
#!/usr/bin/env python3
# Please set "export PYTHONPATH=${CINN_ROOT}/build/python:${PYTHONPATH}" first
import paddle
import unittest
import numpy as np
import cinn
from cinn.frontend import *
from cinn.common import *
from op_test import OpTest
class TestGroup(unittest.TestCase):
def test_group(self):
builder = NetBuilder("matmul")
x_shape = [128, 128, 768]
y_shape = [768, 768]
x = builder.create_input(Float16(),x_shape, "x")
y = builder.create_input(Float16(), y_shape, "y")
out = builder.matmul(
x, y, transpose_x=False, transpose_y=False)
feed_list = [x, y]
fetch_list = [out]
prog = builder.build()
feed_data = [OpTest.random(shape=var.shape(), dtype=var.type()) for var in feed_list]
result = prog.build_and_get_output(DefaultNVGPUTarget(), feed_list, feed_data, fetch_list)
result = [res.numpy(DefaultNVGPUTarget()) for res in result]
for i in range(len(result)):
info_str = fetch_list[i].name()
info_str += ", shape=" + str(result[i].shape)
info_str += ", dtype=" + str(result[i].dtype) + ":\n"
print(info_str)
if __name__ == "__main__":
unittest.main()
5.2 修复 PR
修复思路,是参考主框架将其 y_batch_size=1 && trans_a = False 分支逻辑迁移到CINN中,见 PR:https://github.com/PaddlePaddle/CINN/pull/1407
5.3 收益测试
在 B 机器上测试 Bert 训练的收益:「无明显收益」。借助Nsight工具跑出了 timeline,经过分析发现「GPU空白依旧存在」。

5.4 咨询Nvidia同学
咨询了英伟达的同学,反馈说:「 cuBLAS 第一次將kernel 加載進內存,所以時間較長」,反馈主框架中引入了cublaslt,同时对api 内的deacriptor 创建进行了cache操作,可能有用,但CINN中是没有这个机制的。
要解决这个问题,可以参考主框架实现 AutoTune + Cache 机制:

cuBlas API Launch Latency 耗时异常分析记录的更多相关文章
- Android异常分析(转)
关于异常 异常? 异常就是一种程序中没有预料到的问题,既然是没有预料到的,就可能不在原有逻辑处理范围内,脱离了代码控制,软件可能会出现各种奇怪的现象.比如:android系统常见异常现象有应用无响应. ...
- Java 异常分析
Java 异常分析 本文是对以下内容的分析: Java异常设计 Java 异常分类 Java异常可以告诉什么问题 Java异常处理最佳实践 Java Exception 是为了处理应用程序的异常行为而 ...
- 4种API性能恶化根因分析
摘要:服务发生性能恶化时,需要投入大量人力分析性能异常根因,分析成本高,耗时长.我们提出了一种先在异常调用链内部分析候选根因,再在全局拓扑环境下对候选根因进行汇聚的二级分析方法,克服了调用链之间异常相 ...
- MySQL 外键异常分析
外键约束异常现象 如下测例中,没有违反引用约束的插入失败. create database `a-b`; use `a-b`; SET FOREIGN_KEY_CHECKS=0; create tab ...
- Java ConcurrentModificationException 异常分析与解决方案
Java ConcurrentModificationException 异常分析与解决方案http://www.2cto.com/kf/201403/286536.html java.util.Co ...
- CPU异常分析(以trap00为例)
Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html CPU异常的记录(trap00为例) 一.CPU检测到除零异常的执 ...
- Log4Net异常日志记录在asp.net mvc3.0的应用
前言 log4net是.Net下一个非常优秀的开源日志记录组件.log4net记录日志的功能非常强大.它可以将日志分不同的等级,以不同的格式,输出到不同的媒介.本文主要是简单的介绍如何在Visual ...
- Linux Kernel Oops异常分析
1.PowerPC小系统内核异常分析 1.1 异常打印 Unable to handle kernel paging request for data at address 0x36fef31eFa ...
- IOS异常日志记录与展现功能
在平常的APP开发过程中经常碰到程序遇到异常闪退的问题,通过日志可以把相关的详细错误信息进行记录,本实例要记录不管在哪个页面出错都要进行记录,这边使用到的日志记录插件CocoaLumberjack,以 ...
- java.net.SocketException:Software caused connection abort: recv failed 异常分析 +socket客户端&服务端代码
java.net.SocketException:Software caused connection abort: recv failed 异常分析 分类: 很多的技术 2012-01-04 12: ...
随机推荐
- Tarjan 算法——图论学习笔记
Part.1 引入 在图论问题中,我们经常去研究一些连通性问题,比如: 有向图的联通性:传递闭包--Floyd 算法: 有向图连通性的对称性:强联通分量(SCC)--Tarjan 算法缩点: 无向图的 ...
- Python 潮流周刊第 44 期(摘要)+ 赠书 5 本《明解Python算法与数据结构》
本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...
- 【已解决】Hadoop_03 解决Hadoop输入jps没有NameNode的问题
问题描述: 解决方案: 1.先运行 stop-all.sh 2.格式化 namdenode(在这之前要先删除原目录,即core-site.xml下配置的<name>hadoop.tmp.d ...
- 全面指南:技术写作与编辑工具 Markdown、Git 研究工具
技术写作工具 在技术写作领域,"工具"是指技术写作者用于创建.管理和发布高质量技术文档的各种软件和应用程序.这包括文字处理器.桌面出版应用程序.XML 编辑器.内容管理系统等等.一 ...
- 震撼!这个Python模块竟然能自动修复代码!
说到Python的强大的地方,那真的是太多了,优雅.简洁.丰富且强大的第三方库.开发速度快,社区活跃度高等,所以才使得Python才会如此的受欢迎. 今天给大家介绍一个特别暴力的Python库: Fu ...
- 【FAQ】接入华为帐号服务过程中常见问题总结
华为帐号服务(Account Kit)为开发者提供简单.安全的登录授权功能,用户不必输入帐号.密码和繁琐验证,就可以通过华为帐号快速登录应用,即刻使用App.这篇文章收集了开发者们集成华为帐号服务中会 ...
- sql 语句系列(众数中位数与百分比)[八百章之第十五章]
众数 众数就是出现最多的那个数. select sal,count(*) as cnt from emp where DEPTNO=20 group by sal 通过分组把他们的行数计算出来.那么最 ...
- c# 优化代码的一些规则——什么情况下应该使用new[七]
前言 new 在重构这本书中写道new就是坏代码的味道,说明使用new的情况并不多. 在这里我指的new 是方法修饰符,而不是指实例. 正文 看下new的作用: new 修饰符可以重新定义从基类继承下 ...
- MySQL组合索引
MySQL组引合索优化SQL 我的场景 200w左右的数据,后面会更多 使用定时任务爬取数据插入到自己的数据库.要保证数据的唯一性,所以我用了组合唯一索引. 表结构 最初的组合索引 SQL执行和exp ...
- 简述Linux磁盘IO
1.什么是磁盘 在讲解磁盘IO前,先简单说下什么是磁盘.磁盘是可以持久化存储的设备,根据存储介质的不同,常见磁盘可以分为两类:机械磁盘和固态磁盘. 1.1 机械磁盘 第一类,机械磁盘,也称为硬盘驱动器 ...