cuGraph C++ 概览#

cuGraph 包含一个 C++ 库,它提供了 GPU 加速的图算法,用于处理稀疏图。

术语#

本节定义了 cuGraph 中使用的术语

COO#

COOrdinate 格式是表示图数据的标准格式之一。在 COO 格式中,图表示为源顶点 ID 数组、目标顶点 ID 数组以及可选的边权重数组。边 i 由 source_vertex_id[i]、destination_vertex_id[i] 和 weight[i] 标识。

MORE#

目录结构和文件命名#

外部/公共 cuGraph API 根据功能分组到 cugraph/cpp/include/ 中适当命名的头文件中。例如,cugraph/cpp/include/graph.hpp 包含(旧版)图对象的定义。注意使用 .hpp 文件扩展名来指示 C++ 头文件。

头文件应使用 #pragma once include guard。

文件扩展名#

  • .hpp : C++ 头文件

  • .cpp : C++ 源文件

  • .cu : CUDA C++ 源文件

  • .cuh : 包含 CUDA 设备代码的头文件

头文件和源文件应使用 .hpp.cpp 扩展名,除非它们必须由 nvcc 编译。.cu.cuh 文件编译成本更高,因此我们希望尽可能少地使用这些文件,仅在必要时使用。需要使用 .cu.cuh 文件的一个良好指标是包含 __device__ 和其他只有 nvcc 才能识别的符号。另一个指标是 Thrust 算法 API,其设备执行策略(在 cuGraph 中总是 rmm::exec_policy)。

代码和文档风格及格式化#

cuGraph 代码除少数情况外,所有名称都使用 snake_case:单元测试和测试用例名称可以使用 Pascal case,即 UpperCamelCase。我们不使用 Hungarian notation,但以下示例除外

  • 如果能使意图更清晰,设备数据变量应以 d_ 作为前缀

  • 如果能使意图更清晰,主机数据变量应以 h_ 作为前缀

  • 定义类型的模板参数应以后缀 _t 结尾

  • 私有成员变量通常以后缀下划线结尾

template <typename graph_t>
void algorithm_function(graph_t const &g)
{
  ...
}

template <typename vertex_t>
class utility_class 
{
  ...
 private:
  vertex_t num_vertices_{};
}

C++ 格式化使用 clang-format 执行。您应该在您的机器上配置 clang-format,使其使用 cugraph/cpp/.clang-format 配置文件,并在提交之前对所有更改的代码运行 clang-format。最简单的方法是配置您的编辑器在“保存时格式化”。

本文档中未讨论且无法自动强制执行的代码风格方面通常会在代码评审期间发现,或不强制执行。

C++ 指南#

总的来说,我们建议遵循 C++ Core Guidelines。我们还建议观看 Sean Parent 的 C++ Seasoning 演讲,我们努力遵循他的规则:“没有裸循环。没有裸指针。没有裸同步原语。”

  • 优先使用 STL 和 Thrust 的算法,而不是裸循环。

  • 优先使用 cugraph 和 RMM,而不是裸指针和裸内存分配。

文档在 文档指南 中讨论。

Include 指令#

以下指南适用于组织 #include 行。

  • 按库分组 includes (例如 cuGraph, RMM, Thrust, STL)。clang-format 将遵守分组并在组内按字典序排序 individual includes。

  • 使用空行分隔组。

  • 从“最近”到“最远”排序组。换句话说,本地 includes,然后是来自其他 RAPIDS 库的 includes,然后是来自相关库的 includes,例如 <thrust/...>,然后是随 cuGraph 安装的依赖项的 includes,最后是标准头文件(例如 <string>, <iostream>)。

  • 使用 <> 而不是 "",除非头文件与源文件在同一目录中。

  • 诸如 clangd 之类的工具通常会在可能时自动插入 includes,但它们通常会弄错分组和括号。

  • 始终检查 includes 是否仅对于包含它们的文件是必要的。尽量避免过度包含,尤其是在头文件中。删除代码时仔细检查这一点。

cuGraph 数据结构#

cuGraph 中的应用程序数据包含在图对象中,但在开发 cuGraph 代码时,您将使用各种其他数据结构。

视图与所有权#

资源所有权是 cuGraph 中的一个基本概念。简而言之,“拥有”对象拥有资源(例如设备内存)。它在构造期间获取该资源,并在析构时释放资源 (RAII)。“非拥有”对象不拥有资源。cuGraph 中所有以 *_view 后缀结尾的类都是非拥有类型。

rmm::device_memory_resource#

cuGraph 通过 RMM 内存资源 (MR) 分配所有设备内存。有关详细信息,请参阅 RMM 文档

#

CUDA 流尚未在外部 cuGraph API 中公开。

我们目前正在研究公开此功能的最佳技术。

内存管理#

cuGraph 代码通常避免使用裸指针和直接内存分配。使用 RMM 类,它们旨在利用 device_memory_resource(*) 进行设备内存分配并实现自动化生命周期管理。

rmm::device_buffer#

使用 device_memory_resource 分配指定数量的无类型、未初始化的设备内存字节。如果未显式提供资源,则使用 rmm::mr::get_current_device_resource()

rmm::device_buffer 可在流上移动和复制。复制会在指定的流上对 device_buffer 的设备内存执行深拷贝,而移动则将设备内存的所有权从一个 device_buffer 转移到另一个。

// Allocates at least 100 bytes of uninitialized device memory 
// using the specified resource and stream
rmm::device_buffer buff(100, stream, mr); 
void * raw_data = buff.data(); // Raw pointer to underlying device memory

// Deep copies `buff` into `copy` on `stream`
rmm::device_buffer copy(buff, stream); 

// Moves contents of `buff` into `moved_to`
rmm::device_buffer moved_to(std::move(buff)); 

custom_memory_resource *mr...;
// Allocates 100 bytes from the custom_memory_resource
rmm::device_buffer custom_buff(100, mr, stream); 

rmm::device_uvector<T>#

类似于 rmm::device_vector,它在设备内存中分配连续的元素集,但存在关键差异

  • 作为一种优化,元素未初始化,并且在构造时不会发生同步。这将 T 类型限制为可平凡复制的类型。

  • 所有操作都是流有序的(即,它们接受一个 cuda_stream_view 来指定执行操作的流)。

命名空间#

外部#

所有公共 cuGraph API 都应放在 cugraph 命名空间中。示例

namespace cugraph{
   void public_function(...);
} // namespace cugraph

内部#

许多函数不适合公共使用,因此根据情况将其放在 detail 命名空间或匿名命名空间中。

detail 命名空间#

将在多个翻译单元(即源文件)中使用的函数或对象,应在内部头文件中公开并放在 detail 命名空间中。示例

// some_utilities.hpp
namespace cugraph{
namespace detail{
void reusable_helper_function(...);
} // namespace detail
} // namespace cugraph

匿名命名空间#

仅在单个翻译单元中使用的函数或对象,应在使用它的源文件中的匿名命名空间中定义。示例

// some_file.cpp
namespace{
void isolated_helper_function(...);
} // anonymous namespace

匿名命名空间绝不应在头文件中使用。

错误处理#

cuGraph 遵循惯例(并提供实用工具),用于强制执行编译时和运行时条件以及检测和处理 CUDA 错误。错误通信始终通过 C++ 异常进行。

运行时条件#

使用 CUGRAPH_EXPECTS 宏来强制执行正确执行所需的运行时条件。

用法示例

CUGRAPH_EXPECTS(lhs.type() == rhs.type(), "Column type mismatch");

第一个参数是预期在正常条件下解析为 true 的条件表达式。如果条件评估为 false,则表示发生了错误,并且会抛出 cugraph::logic_error 实例。第二个参数是发生的错误的简短描述,用于异常的 what() 消息。

有时,如果达到特定的代码路径,则无论如何都应指示错误。例如,switch 语句的 default 情况通常表示无效的替代方案。对于此类错误,请使用 CUGRAPH_FAIL 宏。这实际上与调用 CUGRAPH_EXPECTS(false, reason) 相同。

示例

CUGRAPH_FAIL("This code path should not be reached.");

CUDA 错误检查#

使用 CUDA_TRY 宏检查 CUDA 运行时 API 函数是否成功完成。如果 CUDA API 的返回值不是 cudaSuccess,此宏将抛出 cugraph::cuda_error 异常。抛出的异常在其 what() 消息中包含 CUDA 错误代码的描述。

示例

CUDA_TRY( cudaMemcpy(&dst, &src, num_bytes) );

编译时条件#

使用 static_assert 来强制执行编译时条件。例如,

template <typename T>
void trivial_types_only(T t){
   static_assert(std::is_trivial<T>::value, "This function requires a trivial type.");
...
}