基础#

GraphBLAS 规范是一个数学上严谨、经过深思熟虑的 API。大多数 GraphBLAS 函数遵循以下一般模式:

../_images/GraphBLAS-API-example.png

函数的名称对于每个函数显然是唯一的,但通常输出、掩码、累加器、操作符、输入参数和描述符对于每个GraphBLAS函数都是通用的。

输出是将用计算结果更新的对象。它必须已经存在并已初始化,即使初始化为空对象。

mask 是可选的,如果定义了,它控制输出的哪些元素被更新。在 mask 之外的元素保持不变。

累加器是可选的,如果定义了,它决定了输出中的现有值如何与计算中的新值结合。

operator 是 GraphBLAS 中的秘密武器,它允许矩阵乘法使用不同于标准的 plus_times 的半环,或者使用不同于标准加法的方法将两个向量相加。

输入是参与计算的对象。可能有两个输入(用于加法或矩阵乘法)或可能有一个输入(用于应用或归约)。

描述符是一组按位标志。

  • 第一个输入可以在计算过程中进行转置(仅适用于矩阵输入)。

  • 第二个输入在计算过程中可以转置(仅适用于矩阵输入)。

  • 掩码可以被补充,这是为了避免只有少数元素属于掩码的情况。与其创建一个大部分密集的对象,可以提供非常稀疏的掩码并带有补充指示器。

  • 掩码的结构可以用作掩码,忽略掩码对象中的值。这比检查每个值更高效,基本上将掩码视为每个元素都为True。

  • 替换模式表示在最终输出中应清除遮罩区域外的元素。 当不在替换模式时,遮罩外的元素保持不变。

更多详情,请查看官方API规范在graphblas.org

C到Python映射#

C API 和 python-graphblas API 之间的映射如下。

../_images/GraphBLAS-mapping.png

所有影响输出的参数都分组在<<的左侧。 这包括输出本身以及掩码、累加器和描述符的相关部分。

所有描述实际计算(操作、输入参数、运算符)的内容都分组在 << 的右侧。

这种分离是非常有意的。虽然整个表达式作为单个调用传递给后端的GraphBLAS实现,但实际上是为了效率而将两个操作融合在一起。右边的计算可以完全计算出来,随后可以用计算出的对象更新输出。当使用掩码时,这显然是低效的,因为右边计算的许多元素将被丢弃。这就是为了性能而将两个操作融合在一起的原因。然而,在python-graphblas表示法中保持它们的分离,使得更容易看到和理解融合操作的两个方面。

描述符位标志在C语言中很常见,但在Python中我们可以有更优雅的语法。位标志成为它们影响对象的属性。

  • 对于输入参数,添加 .T 表示该参数在操作中应进行转置。

  • 对于掩码,.S 表示仅考虑结构,而 .V 表示应使用每个元素的值。

  • 掩码还有一个前缀 ~ 来表示掩码的补码。

  • 替换模式由一个名为 replace 的布尔关键字参数表示。

更新符号 (<<)#

为了在赋值语法的左侧分离影响输出的项目,显而易见的选择是使用M = ...。不幸的是,在Python中,这会替换变量M所指向的内容,而不是更新M

# This doesn't actually update M
M = semiring.min_plus(A @ B)

Python 确实有使用方括号的赋值符号。虽然这可以用于更新 M,但方括号符号缺乏关键字参数的能力。像这样做是不可能的:

# This will raise a SyntaxError
M[mask, accum=binary.min] = semiring.min_plus(A @ B)

为了充分利用关键字参数的功能,决定在输出对象上使用调用语法(即括号)。Python 不允许将值赋给函数调用的结果。然而,左移(<<)运算符可以与此类调用的结果一起使用。

因此,选择(滥用)左移运算符来表示更新。它满足了从右侧流向左侧的计算视觉感受。左移运算符的结果被忽略,而左侧的对象作为副作用被更新。

# This is the preferred syntax for python-graphblas
M(mask, accum=binary.min) << semiring.min_plus(A @ B)

对于任何反对以这种方式使用左移运算符的人,存在一个更新方法来执行相同的任务。

# This is equivalent to the code above
M(mask, accum=binary.min).update(semiring.min_plus(A @ B))

延迟对象#

在python-graphblas中,大多数计算都会产生延迟对象。只有当双方都传递到更新(<<)时,所有内容才会合并成一个对GraphBLAS后端的调用。这带来了更好的Python语法和非常高效的融合GraphBLAS调用的性能优势。

用户在检查这些延迟对象时需要注意。__repr__ 非常相似, 但通常会显示类似 MatrixExpression 的内容来指示延迟对象。对于小对象, __repr__ 通常会执行计算以向用户显示结果。对于大对象, 为了避免在对象检查期间进行昂贵的计算,不会这样做。

延迟对象也可以使用.new()方法显式转换为真实对象。

# This is a delayed object
delayed = semiring.min_plus(A @ B)
# This is now a real object
real_object = delayed.new()

# Or make it real in one go
M = semiring.min_plus(A @ B).new()

通常,延迟对象可以作为输入传递到后续计算中,而不会出现问题。

# C is a delayed object
C = semiring.min_plus(A @ B)

# Passing C is okay here; D is still a delayed object
D = C.reduce_rowwise(monoid.plus)

然而,延迟对象不能直接用作输出对象,除非先将其转换为真实对象。

函数式调用 vs. 方法调用语法#

一些操作(例如 reduce)只有方法形式的调用。

v << A.reduce_columnwise(monoid.times)

其他操作(例如赋值)只有语法形式。

# Fill the 2nd column of B with all 1's
B[:, 1] << 1

然而,大多数操作都有基于函数和基于方法的调用语法。 对于基于方法的调用,操作符是一个参数。对于函数调用,操作符 是调用对象。

# Method form for element-wise intersection
C << A.ewise_mult(B, binary.plus)

# Functional form for element-wise intersection
C << binary.plus(A & B)

比较对象#

比较对象可能会很棘手,因为需要检查两个对象的形状、稀疏模式和值。有两个便捷的方法可以简化比较。

  1. isequal – 用于检查完全匹配

  2. isclose – 用于检查浮点数据类型的近似相等

默认情况下,只要值相等,类型不需要匹配。如果需要精确的dtype匹配,请将check_dtype关键字参数设置为True。

while True:
    w = ... # perform some computation
    # Exit the loop once converged
    if v.isclose(w, rel_tol=1e-03):
        break