跳转至

精修依赖图

去掉一条并不真实的边、跳过一个并不需要的任务,以及让调度器提前开始。

前置声明一条边

Concept

前面几页把图建了起来。本页从三个方向改动它,而这三者是真正不同的操作 —— 拿错工具是最常见的错误:

你想要 该用
去掉一条推出来的边,因为它不是真实依赖 退出跟踪manual_scopemanual_dep=Truepl.no_dep
当运行期的值这么说时,这个任务根本不要跑 派发谓词pl.submit / pl.spmd_submit / pl.spmd 上的 predicate=
同一张图,但更早派发 调度提示allow_early_resolve=

只有第一类改变正确性保证。谓词改变的是"跑什么";提示除了时序什么都不改。

Quickstart:三种粒度的退出

with pl.manual_scope():                              # whole region: every task inside
    ...

t = pl.create_tensor(..., manual_dep=True)           # one tensor, its entire lifetime

with pl.at(level=pl.Level.CORE_GROUP, no_dep_args=[shared]) as tid:   # one tensor, one task
    ...

三种里有两种可以从 @pl.jit 入口直接写出来,作用在四次迭代、各写输出的互不相交行带上:

import pypto.language as pl
import torch
from pypto.runtime import RunConfig

N, TILE, COLS = 4, 64, 128
ROWS = N * TILE
CFG = RunConfig(platform="__PLATFORM__")
torch.manual_seed(0)
A = torch.randn(ROWS, COLS, dtype=torch.float32)


def check(kernel):
    out = torch.zeros(ROWS, COLS, dtype=torch.float32)
    kernel(A, out, config=CFG)
    torch.testing.assert_close(out, A * 2.0, rtol=1e-4, atol=1e-4)
@pl.jit
def narrow(a: pl.Tensor, out: pl.Out[pl.Tensor]):
    for i in pl.range(N):
        with pl.at(level=pl.Level.CORE_GROUP, no_dep_args=[out]):   # one tensor, one task
            t = pl.load(a, [i * TILE, 0], [TILE, COLS])
            pl.store(pl.mul(t, 2.0), [i * TILE, 0], out)
    return out


@pl.jit
def region(a: pl.Tensor, out: pl.Out[pl.Tensor]):
    with pl.manual_scope():                                          # whole region
        for i in pl.range(N):
            with pl.at(level=pl.Level.CORE_GROUP):
                t = pl.load(a, [i * TILE, 0], [TILE, COLS])
                pl.store(pl.mul(t, 2.0), [i * TILE, 0], out)
    return out


check(narrow)
check(region)

这里两种都算对,是因为那些行带确实互不相交。这既是要点也是危险:两种构造都不做检验,所以换成真正重叠的区域,这条断言在运气好的那一天照样会过。

构造 退出范围 可用于
with pl.manual_scope(): 区域内每一个任务 @pl.jit@pl.function
pl.create_tensor(..., manual_dep=True) 单个张量,其整个生命周期 @pl.jit@pl.function
pl.at(..., no_dep_args=[t]) 单个张量,仅对单个任务 @pl.jit@pl.function
调用实参处的 pl.no_dep(t) 单个张量,仅对单个任务 @pl.program

优先选能表达该断言的最窄那一种。让单个实参退出跟踪说的是"这个任务的这个实参没有冲突";manual_scope 说的是"这块区域的整张图归我",是大得多的承诺。

均为片段:每一行都应位于一个 Orchestration 函数体内。

Mechanics

pl.no_dep

一个由解析器识别的标记,写在 kernel 调用的实参位置 —— 运行期它原样返回该张量。它让运行时对这个实参同时跳过 OverlapMap 的依赖查询生产者插入。

无论被调方把该参数声明为 InOut 还是 InOut,它都合法,因为你断言的是一件带外的事:这个槽位上不存在写后读、写后写或读后写冲突。典型场景是写偏移由数据决定的写入 —— 编译器无法证明不相交,但分配协议保证了它。

这样包裹调用实参需要一个显式的 self.<kernel> 调用,因此它属于 @pl.program 写法。在 @pl.jit 函数里对应的写法是在外层 pl.at 作用域上写 no_dep_args=[t] —— 这也正是 kernel 调用由 outliner 合成、没有语法上的实参槽可包裹时所用的写法。

deps= 收 TaskId,no_dep_args= 收张量。二者不是一件事的两种拼写。

predicate=

pl.submitpl.spmd_submitpl.spmd 都带谓词 —— 但 pl.at 没有。在 @pl.jit 函数里,带谓词的那个写法是 pl.spmd;区域需要谓词时用它,而不是 pl.at

用来跳过那些"需不需要做"只有运行期才知道的任务。调度器在派发点求值 —— 此时依赖已满足,所以取到的值是最新的,而不必在编排期等待。为假时任务就地退休、根本不下发到核上,同时其 fanin 与 fanout 照常结算,下游消费者正常解锁。

out, tid = pl.spmd_submit(self.expert_ffn, tokens, out, core_num=N,
                          deps=[gather_tid],
                          predicate=(row_count[e] > 0))

这个比较是按语法匹配、从不求值的。在这个位置上,row_count[e] > 0 是交给调度器的一份声明,而不是一次 tensor.read 加一次比较 —— 在编排里读它意味着要等这个张量,而那正是谓词想避免的事。

可表达的只有 tensor[indices] OP int 字面量:单个比较,运算符为 == != > < >= <=。不支持链式比较、算术或布尔组合 —— 运行时只支持单个比较。更复杂的条件请在前一个 kernel 里归约成一个门控值,再对它做谓词。

契约: 操作数张量的生产者必须在本任务的 deps= 之中,这样派发点读到的才是当前值。解析器在静态可证的范围内强制这一点;其余由你负责。在 pl.spmd 区域上这就强制要求 as tid 形式:只有那一种拼写接受 deps=with pl.spmd(n, deps=[...]):for i in pl.spmd(n, deps=[...]): 都会被直接拒绝。裸的 with pl.spmd(n, predicate=...): 能解析通过,但它没有办法点名生产者,因此契约完全由你负责。

allow_early_resolve=

把任务标记为可推测早派发的生产者:调度器可以在它完成之前把它的消费者预置到空闲核上,等它一完成就用门铃放行。这是生产者侧的提示 —— 消费者只有在它所有生产者都被标记(或已完成)之后才会预置。

纯调度行为:不影响结果。在由大量短任务构成的关键路径上收益明显,其余情况下无害。sync_start 的 SPMD 任务自身不能被逐 block 预置,但标记它仍然能让它的消费者预置。

pl.system.task_dummy

一个不做任何工作的依赖汇聚点:它接受 deps=[...] 并返回一个 TaskId,因此可以把若干生产者收敛成一个句柄,供后续任务点名。

gate = pl.system.task_dummy(deps=[tid_a, tid_b])
out, _ = pl.submit(self.consumer, x, out, deps=[gate])

pl.submit 一样,它是解析器构造 —— 在被装饰函数体外调用会抛异常。注意拼写:它在 pl.system 下,不在顶层。

边界情况

致命陷阱:

  • pl.no_dep 是一个编译器无法检验的断言。如果那些区域其实并非互不相交(即确实有重叠),你就删掉了一条真实的边,结果是竞态 —— 与压根没声明这条边是同一类缺陷。
  • 对一个生产者不在本次 submit deps= 里的张量使用 predicate=,读到的是内存里当时恰好存在的东西。没有任何提示;任务被跳过或不被跳过,取决于陈旧数据。
症状 可能原因 修复
加了 no_dep 之后出现竞态 那些区域其实并非互不相交,确实有重叠 去掉这个标记;它删掉的那条边是真的
@pl.jitpl.no_dep 破坏元数据推断 包裹层让 @pl.jit 的 shape/dtype 推断看不到该张量 改在外层 pl.at 作用域上写 no_dep_args=[t]
谓词被解析器拒绝 可表达的只有 tensor[indices] OP int 字面量 在前一个 kernel 里归约成一个门控值,对它做谓词
被谓词的任务在不该跑时跑了 操作数的生产者不在 deps= 把生产者的 TaskId 加进 deps=
pl.cluster()predicate / allow_early_resolve / timing_slot 被拒绝 cluster 内嵌的 pl.spmd 不产生可承载该提示的 Submit 把提示移出 cluster
allow_early_resolve 没有任何效果 消费者只有在它所有生产者都被标记后才预置 把其余生产者也标记上,或接受它在此不适用
pl.task_dummy 未定义 它在 pl.system 调用 pl.system.task_dummy(deps=[...])

See Also