UE5.0 AsyncCompute异步计算优化简易教程

​ 现代图形API(DX12/Vulkan)以队列为单位提交录制好的Command Buffer, 通常GPU上至少有三种独立的队列类型:Graphics(gfx), Compute, Copy。

​ 提交到队列中的命令并不保证严格的执行先后顺序,但Pass之间总是需要插入Barrier做好同步的。而Pass本身用到的SM Warp有限,因此,在Pass执行过程中,通常会出现Main Graphics队列忙碌,但Warp使用量较低(SM Occupancy低)的情况。

​ 因此,找到出现这种情况的区域,并插入对应的Async Compute Pass,能让SM Occupancy大幅度提升,同时也减少了Main Graphics Queue的任务数量,提升帧率。

​ 以ShadowDepth绘制Pass为例,在Main Graphics Queue绘制Shadow Depth前,插入了Async SSAO的计算任务,截帧如下:

Image

​ ShadowDepth Pass绘制过程中SM Oc非常的低,插入Async SSAO后,可以在异步计算队列里利用这部分 Unused Warp,同时不会阻塞Main Graphics Queue的任务,四舍五入等于白嫖一份SSAO的计算时间。

​ 当然不是任何时候插入Async Compute都有优化效果的,每次提交到异步队列中的Command buffer都需要和主队列做好Sync同步,随意插入Async compute, 插入的Semaphore可能需要Main Graphics等待一段时间。

​ 如下所示,异步HZB Build放到了硬件Occlusion Culling之前,由于硬件Culling需要DepthStencil Buffer(Depth ReadOnly Stencil Write),HZB Build的第一次build需要原始的Depth Buffer SRV,(我个人感觉不应该在这里插入semaphore)RDG会插入一个Depth Resource的Barrier,导致MainGraphics的硬件Culling需要等待HZB第一次Build结束后才执行,同时也导致GPU出现一大段Idle warp区域(灰白色):

Image

UE4中如何插入Async Compute

​ RHI中创建时就已经根据硬件信息准备好异步队列对应的Commandlist了,透过静态方法FRHICommandListExecutor::GetImmediateAsyncComputeCommandList()暴露出来。UE 4.27下纯RHI调用方法如下:

// Main graphics command list.
FRHICommandListImmediate& RHICmdList;

// UAV Resource queue owenership transfer.
auto* UAVTransition = RHICreateTransition(ERHIPipeline::Graphics, ERHIPipeline::AsyncCompute, ...);
auto* SRVTransition = RHICreateTransition(ERHIPipeline::AsyncCompute, ERHIPipeline::Graphics,....);

// Start MainGraphics --> AsyncCompute
RHICmdList.BeginTransition(UAVTransition); 

auto& RHICmdListComputeImmediate = FRHICommandListExecutor::GetImmediateAsyncComputeCommandList();
{
    // Wait when MainGraphics --> AsyncCompute transition finish.
    RHICmdListComputeImmediate.EndTransition(UAVTransition);
    
    // Do some async compute here, and write resource to uav.
    DispatchComputeShader(RHICmdListComputeImmediate, ...);
    
 	// Async compute finish..............
    
    // Start AsyncCompute --> MainGraphics
    RHICmdListComputeImmediate.BeginTransition(SRVTransition);
}
FRHIAsyncComputeCommandListImmediate::ImmediateDispatch(RHICmdListComputeImmediate);

// Do other graphics pass.

// When need sync
AddPass(GraphBuilder, [this](FRHICommandList& RHICmdList)
{
	//Main Graphics Wait AsyncCompute --> MainGraphics
	RHICmdList.EndTransition(SRVTransition);
});

​ 纯RHI的做法可以自由安排同步时机,可以用来做一些跨帧异步计算的操作,比较灵活。

​ 在RDG中调用Async Compute方法仅需在Add Pass时增加标记ERDGPassFlags::AsyncCompute.

Image

​ 如果想手动填充Pass Lambda,需保证AddPass的类型参数为(FRHIComputeCommandList& RHICmdList):

Image

​ RDG内部做了类型萃取,编译时仅该类型的Lambda Pass才会分发到Async Compute,否则覆写Flag为Main Graphics的Compute.

Image

​ RDG会自动处理资源的转换,但有时候插入的Semaphore可能不太理想,因此需要Nsight仔细检查Main Graphics每一处的Sync,减少阻塞。

UE4 Async Compute转换麻烦的地方

#1. 不少Pass Parameter的组织方式还是原来的LAYOUT_FIELD方式,SetParameter函数也是用的FRHICommandList,而异步计算提供的CommandList类型为FRHIComputeCommandList,两者的继承关系为:

Image

因此没有办法简单复用原有的代码,而UE的效果一般变体都很多,Pass的复杂度也很高,改成Async Compute工作量很大。

#2. UE4 不是每个效果都提供了Compute Shader版本,如果要改成Async Compute,在考虑原有效果不变的情况下,要充分利用Shared Memory加速,写出一个效率高的CS还是挑战性很高的。

换言之,这就是个纯粹的搬砖活。

如何抓帧Profile Async Compute

首先我们需要打包游戏,一般Development包即可,Shipping包的一些Marker信息可能会丢失不便Profile。

一般使用两个软件抓帧:PIX和Nsight.

PIX抓帧比较简单,就正常用Pix启动游戏然后按下printscreen按钮即可。

然后我们得到一个wpix捕获。

Image

一定要按照如图顺序启动,不然UI会变乱。

然后我们就能得到一份运行柱状图了:

Image

注意,在Graphics Queue0中的Wait有可能是PIX自己插进来的Fence,可以点击那个Wait等待的Fence Object,如果没有DebugName,则是PIX创建的。(UE4 DX12 RHI所有Fence创建都有DebugName).

Image

Nsight捕获步骤如下:

Image

Nisght捕获的信息较全,按我的经验来,最重要的部分如下三个:

Image

第一个是3D Queue的Sync,通常来说,Async Compute队列从来不应该让3D queue等待,如果出现了Sync部分,就需要改调度代码了。

第二个是SM Occupancy, 指示了Idle Warp和Unused Warp的数目,理想情况下越高越好,如果有些区域出现很多空闲的Warp,考虑插入Async Compute.

Image

第三个是 SM Shared memory使用率,片上缓存很珍贵,如果CS耗尽Shared Memory,能同时调度的线程数就会下降,也会减低执行速度。

其余的数据,如L1/L2 Cache Hit, PCIe load, 只能当作一个参考,很多时候算法就是会导致大量的Cache Miss, 并无太大优化参考价值。

© - 2024 · 月光下的旅行。 禁止转载