CUDA Stream优化经验

Multi-Process Service(MPS)原理:

    一个GPU卡上同时只能执行一个context;因此多进程同时往一个GPU卡上提交任务时,同时只能有一个任务跑起来,没法多任务并行;

    MPS服务:多进程提交的任务先提交至MPS服务进程,该进程会把所有任务使用同一个context但不同的stream, 提交给该块GPU卡,使得可以多任务并行;

    缺点:增大了任务提交的延迟,因为要多经过MPS服务进程这个“代理”;

Stream: 任务队列,单个Stream内部FIFO,多个Stream之间及和host之间可overlap执行;

销毁Stream的API, host要blocking到该Stream所有任务执行完毕后,才会执行成功并继续;

尽量不要使用Stream0(defalut stream,有些API不指定stream则默认使用这个),它不能和其他stream并行执行;(其他stream设了cudaStreamNonBlocking者例外)

内存<-->显存Copy要想异步,必须同时满足以下条件:(另外,同方向同时只能有一个Copy在执行)

1. Copy任务必须不能在default stream里;

2. 必须使用Async版本API(cudaMemcpyAsync);

3. Host内存必须是pinned的;

 

同步:

按“狠”的程度从高到低:

    1. cudaDeviceSynchronize: Host等所有stream的所有任务都执行结束,才继续往下走;

    2. cudaStreamSynchronize: Host等这一个stream的所有任务都执行结束,才继续往下走; 

    3. 使用Event来同步;可以让Host等某个stream的某个event,也可以让某个stream等另一个stream的event;

Event有2种状态:发生,没发生;创建时默认是“发生”,cudaEventRecord会把它设为“没发生”,在stream里执行到它这里会把它设为“发生”;(多线程时,注意不要在创建event之前去调用cudaEventRecord,这里易出bug)

如果创建时使用"cudaEventDisableTiming"这个Flag,则该Event被执行到时不进行时间记录,可以节省开销;(我认为此时该event仅用于同步)

同步时对event的使用有3种方法:

    1. cudaEventQuery:Host主动查询event的状态;

    2. cudaEventSynchronize:Host blocking住,等待该event执行到才继续走;

    3. cudaStreamSynchronize:另一个stream blocking住(Host继续执行不blocking),等待该event执行到才继续走;

CUDA_LAUNCH_BLOCKING=1环境变量可以让所有stream变成对Host而言是同步执行(即Host发射一个任务,就等着该任务执行完,Host才能继续往下走);用于debug时;

Profiling工具:

    Windows上:Nsight Visual Studio版;NVIDIA Visual Profiler;

    Linux上:Nsight Eclipse版;NVIDIA Visual Profiler; 命令行nvprof;

优化原则:

    1. 让瓶颈设备的“空置率”尽可能低;

    2. Host线程发射任务尽可能早些,让发射和实际执行之间的“空闲”间隔尽可能小;

常见bad-case:

1-A: 启动kernel时忘记指定stream,导致默认stream和其他stream之间串行执行;

1-B: cudaEventRecord时忘记指定stream,导致该event被放进默认stream,导致cudaEventSynchronize时等待默认stream, 而默认stream又要等待其他steam的完成,造成大等待;

1:以上问题的解决:不要再默认stream上放任务;调用API要小心,不要忘记指定stream参数;或者让其他stream创建时指定为cudaStreamNonBlocking;

2-A:先cudaMemcpy,再启动另一个stream上的kernel,导致后者等待前者;因为前者放进了默认stream,要执行完Host才能继续;解决:换成cudaMemcpyAsync交给其他stream即可;

2-B:cudaMemcpyAsync时忘记在主存上使用pinned memory,导致退化为同步copy版本;(Visual Profiler上会显示"Memory Type"是"Pageable")

3:<显存开辟、kernel执行、显存释放>不断迭代,导致kernel要等显存开辟完成才执行,显存释放要等kernel执行完毕才执行(这两者会自动被CUDA检测到?示例代码不会崩溃?);显现出来痕迹是Host执行某些API的时间特别长(例如此例的cudaFree);解决:反复重用显存,减少开辟和释放的次数;

4:Host干活慢,耽误了GPU stream的发射;解决:把活儿交给GPU去做,Host采用多线程/SIMD等技术来加速;

5:kernel执行时间太短,显得Host端执行"cudaLaunch"的时间相对长,时间都浪费在"cudaLaunch"、"cudaLaunch"和真正执行之间的空隙上了;解决:"融合"成大任务、batch,总之让kernel耗时更长些;

6:过度同步:Host很多时候等待在同步API上;解决:合理重构尽量少同步、使用更”不狠“的同步API例如cudaEventSynchronize少用cudaDeviceSynchronize;

7:Profiler开销:减少同步次数和程度?

8:古老的CUDA GPU架构里,所有stream的任务被放到同一个队列的,所以并行程度和任务发射的顺序有关;

stream callback:

新一些的CUDA支持"cudaStreamAddCallback",在该stream执行到这里时,在host端调用这个指定的callback函数;可用于当stream的某任务完成时,通知Host端做些事;

注意:所有stream上注册的callback,都会被同一个driver线程执行,因此是串行的(神似ps-lite的用户callback!);所以callback里尽量只放很轻量级的操作,例如把指针交给线程池里的某个线程并signal它)

你可能感兴趣的:(GPU)