AI 平台/部署

在 NVIDIA OptiX 中使用协作向量实现神经渲染

NVIDIA OptiX 9.0 的发布引入了一项名为 Cooperative Vectors 的新功能,可将 AI 工作流作为光线追踪内核的一部分。该功能利用 NVIDIA RTX Tensor Cores 在着色过程中进行硬件加速的矩阵运算和神经网络计算。这解锁了 NVIDIA RTX Neural Shaders NVIDIA RTX Neural Texture Compression (NTC) 等 AI 渲染技术,并在实时渲染中进一步向电影级逼真材质迈进。

协作向量 API 已在 OptiX DirectX NVAPI Slang Vulkan 中推出。本文将探讨适用于所有 API 的协作向量背后的概念,并通过使用 OptiX API 的示例进行工作。

为何选择矩阵运算?

多层感知器 (MLP) 是许多神经网络算法的基本构建模块。研究表明,MLP 能够忠实地再现其训练所用的效果。即使 MLP 足够小,可以实时运行,它们也能够处理有趣的效果,例如基于物理性质的着色,有时比传统着色网络更快,内存占用更小。

MLP 通常由一个输入向量、几个全连接层和一个输出向量组成。不同层向量的大小不必相同。

Diagram of a multilayer perceptron with one input layer, two hidden layers, and an output layer.
图 1。包含一个输入层、两个隐藏层和一个输出层的 Multilayer Perceptron (MLP)

MLP 评估 (推理) 的每一层都有两个阶段:前一层值的加权和偏差线性组合,以及可选的 非线性激活函数 。加权和偏差线性组合归结为矩阵向量乘法,然后添加偏差向量,也称为 Affine Transform

任何两个 affine 变换的合成都是 affine 变换。这意味着,如果每层只有 affine 线性相加偏置相位,则整个 MLP 始终可以简化为单个 affine 变换。由于在每层的 affine 结果后应用了非线性激活函数,MLP 的表现力远比这更强。

在全连接 MLP 中,每个神经元都是前一层中所有神经元的函数,如 Figure 1 所示。单层的完整概念计算如 Figure 2 所示。

Diagram of the conceptual building blocks of one layer of MLP evaluation: vector-matrix multiply, then addition of a bias vector, and finally application of the nonlinear activation function.
图 2。评估 MLP 的单层通常包括 vector-matrix multiply、添加 bias vector 和应用 nonlinear activation function

为何选择协作向量?

“协作向量的目标之一是支持使用 NVIDIA Tensor Cores 来加速矩阵运算。通常情况下,CUDA SIMT 编程模型需要完整的活动线程束来执行此操作,但光线追踪编程模型独立处理线程,无法保证完整的线程束。此外,Tensor Cores 提供矩阵 – 矩阵乘法,但每个光线追踪线程只需要向量 – 矩阵乘法,这将未充分利用 Tensor Cores。”

此外,CUDA API 需要针对特定硬件版本,并且不能保证在各个架构之间向前兼容。如需了解矩阵乘法的 CUDA 多线程方法,请查看《 Matrix Multiplication Background User’s Guide 》。

协作向量通过提供以下 API 来解决这些限制:

  • 允许使用包含一些不活动线程的 warp 进行矩阵运算
  • 提供跨架构的向前和向后兼容性
  • 支持用户在单个线程中指定向量数据,同时重新映射运算,以更高效地利用 Tensor Cores

协作向量可以处理线程束中的数据和执行离散,但性能会有所下降。当线程束中的 MLP 权重相同,并且线程束中有完整的线程互补时,即可获得最佳性能。使用 Shader Execution Reordering (SER) 可以帮助实现这两个目标。

由于评估 MLP 是一系列向量-matrix 乘法,因此当线程束中的所有线程并排评估同一 MLP 时,协作 vector API 可以将组合线程束的仿射运算视为 matrix-matrix 乘法和 bias。这就是协作意味着:线程捆绑在一起,将多个向量-matrix 运算转化为 matrix-matrix 运算。

outputMatrix = inputMatrix × weightsMatrix + biasMatrix

Diagram of a matrix-matrix multiply, addition of a bias matrix, and  which can be used for parallel execution of a full warp of threads doing MLP layer evaluation.
图 3。全线程束组合 MLP 层评估的仿射部分由矩阵 – 矩阵乘法和偏差矩阵组成

此处,除权重矩阵外,所有矩阵均为 32 行高,输入、输出和 bias 矩阵的每一行均表示单独线程的数据。

在 OptiX 中使用 Cooperative Vectors

协作向量是不透明向量类型,本质上是数组类,可以具有任意长度。OptiX 提供了一种名为 OptixCoopVec 的协作向量的实现。OptiX 中的协作向量支持一组特定且有限的运算,旨在帮助加速 MLP 和小型神经网络的评估。

在协作向量 API 中,使用函数 optixCoopVecMatMul 完成带偏差的向量矩阵乘法,该函数执行层评估的仿射部分。由于通常需要在不同阶段使用不同的激活函数,因此在向量 – 矩阵乘法后单独应用激活函数,并且可以通过协作向量 API 提供的一组向量函数来构建激活函数。

outputVector = inputVector × matrix + bias

Diagram of a vector-matrix multiply followed by addition of a bias vector, the conceptual building block of one layer of MLP evaluation.
图 4。线程的 MLP 层评估的仿射部分包括 vector-matrix multiply 和 addition of a bias vector

所有 RTX 设备和特定服务器级 GPU 上的 OptiX 均支持协作向量。您可以使用 optixDeviceContextGetPropertyOPTIX_DEVICE_PROPERTY_COOP_VEC 查询设备支持。在不支持的设备上使用协作向量将生成错误,因为没有后备支持可用。

实现示例

本节将探讨用于在 OptiX 着色器中执行推理或 MLP 层评估的协作向量 API。我们将学习的示例改编自 OptiX SDK 中的 optixNeuralTexture 示例。此示例使用 NTC SDK ,它可以处理训练 (压缩) 纹理,以指定的文件格式存储权重和偏差,并演示使用不同着色语言在各种情况下的推理 (解压缩) 。您可以使用 NTC SDK 压缩自己的纹理,然后使用 optixNeuralTexture 进行查看。

使用协作向量的代码大量使用 C++ 模板。模板通过提供静态数组大小和编译时已知的静态数据类型,帮助编译器生成高性能代码。提前定义常用类型,使代码更易于阅读。

using T_OUT = OptixCoopVec<float, 16 /*output channels*/>;
...
T_OUT texel = inferTexel<T_OUT> ( 
latents, weights, x, y, ... );

如此一来,您便可使用 T_OUT 类型的快捷键来代替模板化的 OptixCoopVec<> 类型。函数 evalMLP 针对屏幕上的给定像素评估完整的 MLP。在伪代码术语中,它将设置 MLP 的输入,然后评估 MLP 的每一层,最后返回最后一层的输出:

template <class T_OUT>
evalMLP( T_OUT& outLayer, latents, mlpWeights, x, y )
{
    using T_IN  = OptixCoopVec<half, 48 /* input vec size    */ >;
    using T_HID = OptixCoopVec<half, 64 /* hidden layer size */ >;
    T_IN networkInputs = prepareNetworkInputs_FP16<T_IN>(x, y, latents);
    T_HID hiddenOut1 = evalLayer<T_IN, T_HID>(
        networkInputs, mlpWeights, 0, scaleBiasOffset, hiddenOut1);
    T_HID hiddenOut2 = evalLayer<T_HID, T_HID>(
        hiddenOut1, mlpWeights, weightOffset1, scaleBiasOffset, hiddenOut2);
    T_HID hiddenOut3 = evalLayer<T_HID, T_HID>(
        hiddenOut2, mlpWeights, weightOffset2, scaleBiasOffset, hiddenOut3 );
    outLayer = evalLayer<T_HID, T_OUT>(
        hiddenOut3, mlpWeights, weightOffset3, scaleBiasOffset, outLayer);
    return true;
}

请注意每层评估的输出如何成为下一层评估的输入。详细了解层评估:

template <class T_IN, class T_OUT> evalLayer(
    T_IN&           inputArray,
    uint8_t*        weights,
    uint32_t        weightsOffsetInBytes,
    uint32_t&       biasOffsetInBytes,
    T_OUT&          outputArray )
{
  outputArray = optixCoopVecMatMul <
    T_OUT,
    T_IN,
    OPTIX_COOP_VEC_ELEM_TYPE_FLOAT8_E4M3, // inputInterpretation
    MAT_LAYOUT,                           // matrixLayout
    false,                                // transpose
    T_OUT::size,                          // N
    T_IN::size,                           // K
    OPTIX_COOP_VEC_ELEM_TYPE_FLOAT8_E4M3, // matrixElementType
    OPTIX_COOP_VEC_ELEM_TYPE_FLOAT16      // biasElementType
  >(
    inputArray,                           // inputVector
    weights,                              // matrix base ptr
    weightsOffsetInBytes,                 // matrix offset
    weights,                              // bias base ptr, same as weights
    biasOffsetInBytes                     // bias offset
  );

  // increment offset to the next layer
  biasOffsetInBytes += T_OUT::size * sizeof( T_OUT::value_type );

  outputArray = activate<T_OUT>( outputArray );
}

单层计算只不过是对 optixCoopVecMatMul 的封装。矩阵乘法后,将偏移量增加到偏置向量,以便为下一层做好准备 (请注意,偏置偏移量是在此函数中通过参考传递的) 。然后调用层上的激活函数。

您可能在这些代码示例中注意到,我们将相同的 weights base pointer 传递给多次调用 evalLayer 的函数,并将此基指针用于 weights 和 biases。通过向基本指针(本例中为 weightsOffsetInBytesbiasOffsetInBytes)添加常量偏移值,在每个步骤中查找正确的数据。

使用这种方式编写代码有两个原因。第一个原因是,在读取 NTC 格式的文件时,API 会返回一个内存块,其中 weights matrices 和 bias vectors 均被紧密打包,您可以使用简单的运算来迭代每层的数据。第二个原因是,当重复使用相同的 weights base pointers 时,cooperative vectors API 会利用对 optixCoopVecMatMul 的连续调用。编译器将注意到重复使用的 base pointers (即使使用不同的常量偏移量) ,并将优化您的程序,以防止层之间不必要地发生 shuffling 和 unshuffling 操作。

最后,我们来看看 activation function:

template<class T_IN> 
VecT activate(const T_IN& x, bool scaleActivation=true)
{
    T_IN tmp    = optixCoopVecFFMA( x, T_IN( 1.0f/3.0f ), T_IN( 0.5f ) );
    tmp         = optixCoopVecMin( optixCoopVecMax( tmp, 0.0f ), 1.f ); // clamp
    T_IN result = optixCoopVecMin( x, 3.0f );
    result      = optixCoopVecMul( result, tmp );
    if( scaleActivation )
         result = optixCoopVecFFMA( result, T_IN(invStep), T_IN(bias) );
    return result;
}

由于 MLP 激活函数对层输出向量的每个元素应用非线性映射,因此上述代码中调用的协作向量函数均为向量运算,而非矩阵运算。与应用层权重矩阵相比,激活操作通常要小得多,且成本更低。有一组有限的内置向量函数可与通常在 MLP 激活函数中找到的协作向量一起使用,例如 tanh、log2、exp2、min、max、ffma 等。

一些协作向量函数具有采用标量参数的变体,但并非全部。在某些情况下,您需要从标量中创建具有常量值的向量。在这里,这是使用 OptixCoopVec 构造函数完成的,例如,使用 T_IN (0.5 f) 参数进行第一次 optixCoopVecFFMA 调用。这种特定的激活函数来自 NTC SDK 。通常,在设计您自己的网络时,激活可以像调用 optixCoopVecMax 来模拟众所周知的 ReLU 激活一样简单。

神经图形

协作向量用于实现 RTX Neural Shaders RTX Neural Texture Compression 。这些技术可作为 NVIDIA RTX Kit 的一部分提供,NVIDIA RTX Kit 是一套开源资源库,旨在简化这些技术的使用和集成。如需快速了解 RTX Kit (包括每个资源库的链接和资源) ,请参阅 使用 NVIDIA RTX Kit 开始使用神经网络渲染

Rendered scene with a dragon flying over craggy rock cliffs, with a blue sky background.
图 5。使用 NVIDIA NTC SDK 和 NVIDIA RTX Mega Geometry 渲染龙场景

图 5 中描绘的巨龙需要进行纹理压缩,才能容纳 GeForce RTX 5080 GPU 的 16 GB 显存。单是巨龙就有超过 100 个 8K UDIM 纹理,每个纹理有五个层。如果将纹理从文件解压缩到内存中,它们将消耗超过 32 GB 的 VRAM,是 GeForce RTX 5080 上可用内存的两倍多。

借助 NTC,巨龙纹理的内存占用在不到 3 GB 的情况下变得更加合理,大约是 BC 压缩纹理的一半。这为场景中的其余纹理以及几何图形、动画、BVH 和着色器留下了充足的空间,使这个大规模生产规模的场景能够在单个 5080 GPU 上实时渲染。

RTX Neural Shading SDK 中展示了神经网络着色器,其中提供的示例可帮助您学习如何训练自己的神经网络着色,然后在正常图形渲染过程中使用这些着色器执行推理。协作向量可以作为实现 Real-Time Neural Appearance Models 的一种方法。

性能注意事项

请考虑以下事项以获得最佳性能:

  • 混洗和取消混洗: 为利用 Tensor Core,在调用 optixCoopVecMatMul 之前对数据进行混洗,然后取消混洗。在两次此类调用之间,如果您仅使用支持的向量运算,则无需进行解洗和重洗,从而提高性能。
  • 全线程束 :使用全线程束时性能最佳。使用 SER 合并线程,并避免在动态条件内调用 optixCoopVecMatMul
  • 显存布局:权重矩阵布局会显著影响性能。OptiX 支持推理和训练的最佳布局,应使用这些布局以获得最佳性能。使用 optixCoopVecMatrixConvert 将矩阵转换为最佳布局。

使用 OptiX 协作向量进行训练

OptiX 支持使用协作向量进行训练,包括向前和向后传播。有关更多信息,请参阅 OptiX 编程指南 。特别是,两个设备端内部函数 optixCoopVecReduceSumAccumulate 和 tg_ 23 分别有助于累积偏置向量和权重矩阵的损失值。

开始使用

协作向量是 NVIDIA OptiX 数据类型和 API,用于在 OptiX 着色器程序中执行高性能向量和矩阵运算。这些向量和矩阵运算是多层感知器 (MLPs) 等常见机器学习算法的核心。协作向量有助于在 NVIDIA RTX GPU 上使用 Tensor Cores,而这之前需要在线程束中的线程之间进行显式协调。借助协作向量,开发者不再需要使用同步多线程技术。它们可以使用更简单的单线程编程风格,执行对这些神经网络算法至关重要的高效矩阵向量乘法运算。

协作向量可从 NVIDIA OptiX SDK 9.0 开始使用。我们还通过 4 月底的 Agility SDK 预览版、 Vulkan Slang 将协作向量 API 引入 DirectX ,以便您可以在支持硬件加速光线追踪的任何地方使用它们。OptiX 协作向量 API 文档可在 OptiX 编程指南 中获取,可在线获取,也可通过 SDK 和示例分发 PDF 格式。

在 OptiX SDK 中,有一个 RTX Neural Texture Compression 推理示例 (称为 optixNeuralTexture) ,该示例使用 cooperative vectors 在着色过程中实时解压缩神经压缩纹理,与热门的 BC5 或 BC6 压缩纹理格式 相比,可节省 20 倍的内存,与 optixMeshViewer 示例中使用的未压缩纹理相比,可节省 80 倍的纹理占用空间。

随着时间的推移,我们很高兴看到协作向量出现新的有趣用例。加入 OptiX NVIDIA 开发者论坛 的对话,了解更多信息并发布您的体验。

 

标签