跳转至

算子系统

类型 (Type) 安全的算子定义,支持自动类型推导,按模块化分类组织(TensorOp、TileOp、SyncOp、CrossCoreOp)。

算子分类

分类 类型 用途 文件位置
TensorOp TensorType 支持广播的 N 维张量 (Tensor) 操作 src/ir/op/tensor_ops/
TileOp TileType 硬件优化的 Tile 操作 src/ir/op/tile_ops/
SyncOp UnknownType(屏障);ScalarType(task / 启动形状查询) 流水线屏障、同步、TaskId 与 SPMD 启动形状查询 src/ir/op/sync_ops/
CrossCoreOp UnknownType/TileType AIC↔AIV 跨核通信 src/ir/op/sync_ops/cross_core.cpp
PrefetchOp 不透明句柄 (opaque handle) GM→L2 异步预取 src/ir/op/prefetch/prefetch_async.cpp

主要特性:流式 API、自动类型推导、kwargs 元数据、NumPy 风格广播、类型提升、动态维度(kDynamicDim

类型系统

// Dynamic dimensions (pypto/core/common.h)
constexpr int64_t kDynamicDim = -1;
auto dynamic_dim = make_int(kDynamicDim);
类型 维度 用途 内存
TensorType N 维 通用张量、函数参数/返回值 DDR(可选 MemRef)
TileType N 维 统一缓冲区中的硬件优化 Tile 统一缓冲区(可选 MemRef)
ScalarType 0 维 标量值 寄存器
UnknownType 无返回值(同步操作)

REGISTER_OP 流式 API

方法 用途 示例
set_op_category(str) 算子分类 .set_op_category("TensorOp")
set_description(str) 人类可读描述 .set_description("Element-wise add")
add_argument(name, desc) 位置 Expr 参数 .add_argument("lhs", "Left tensor")
no_argument() 无参数(同步操作) .no_argument()
set_attr<T>(name) Kwarg 模式(T: bool, int, DataType 等) .set_attr<bool>("a_trans")
f_deduce_type(fn) 类型推导函数 .f_deduce_type(DeduceAddType)
set_core_affinity(a) 算子在哪个核上执行(放置 .set_core_affinity(core_affinity::CoreAffinity::VECTOR)
set_no_duplicate() 算子不得在第二个核上运行(复制 .set_no_duplicate()
set_arg_effect(i, e) 算子对第 i 个参数缓冲区做了什么 .set_arg_effect(2, ArgEffect::Write)
set_arg_effect(i, fn) 同上,但由 kwarg 决定 .set_arg_effect(2, [](const auto& kw) { ... })
no_arg_writes() 已分类:不通过任何参数写入 .no_arg_writes()
set_write_channel(c) 算子写入所走的硬件通路 .set_write_channel(WriteChannel::Dma)
set_output_arity(N) 产生的值的个数;N > 1 表示结果是 TupleType——参见多输出算子 .set_output_arity(2)
set_workspace_arg(i) i 个参数是编译器提供的暂存空间,而非结果 .set_workspace_arg(2)

参数效应(Argument effects)

消费这些声明的整条链见 参数方向推导

原地更新某个参数的算子必须显式声明。方向推导(direction inference)、依赖分析 (dependency analysis)和参数方向验证器都向注册表询问同一个问题——这次调用是否 写入该参数所指的缓冲区?——而从未回答过的算子会被读成纯消费者:

tile.mscatter 写 output_tensor,却从未声明
  → 它写入的参数方向停留在 In
  → 不会对读取它的 kernel 发出 RAW 边
  → 调度器可以先运行读方
  → 读到陈旧数据,或等待一个无人写入的信号而死锁
效应 含义 示例
ArgEffect::Read 只读,从不写入。未声明参数的默认值 tile.store 的源 tile
ArgEffect::Write 覆盖写,写前不读 tile.storeoutput_tensorpld.tile.getdst
ArgEffect::ReadWrite 既读 tile.matmul_acc 的累加器、原子 store 的目的操作数

部分覆盖仍然是 Write 只写入子区域的 store 不会读取未触及的其余部分——没有 任何数据流内核——所以其目的操作数是纯写。把它声明成 ReadWrite 并非无害的保守 近似:这会让外层参数变成 InOut,从而触发 host→device 搬运,并且在跨 rank 场景下 为两个写入不相交行的 rank 凭空造出一条依赖。

目的操作数是否被读取按算子判定,而非按家族:gather / exchange 的目的窗口只被推入、 从不被 load,因此是 Write;而 reduce 的目的窗口会把运行值 load 回来,因此是 ReadWrite

ReadWrite 留给真正会读取该槽位的算子:累加器(out += x 会读取运行中的和)、 原子 store/assemble,或那些未触及位置会流入 SSA 结果的 destination-passing 算子 (tile.scatterarray.update_element)。

由 kwarg 决定的效应。 当答案取决于某个 kwarg 时,传入 resolver 而非常量。kwarg 不 仅能决定怎么写,还能决定某个实参是否被写:tile.mgather 的第三个操作数在 Mat elem 模式下是被写的 GM scratch,在 Mat row 模式下则是只读的 valid_shape。另外两处是 store 家族的 atomic kwarg,以及 pld.system.notifyop kwarg——后者默认是 atomic-add, 因此未加标注的 notify 会读取它累加的槽位:

REGISTER_OP("tile.store")
    // ... arguments, memory spec ...
    .set_arg_effect(2,
                    [](const std::vector<std::pair<std::string, std::any>>& kwargs) {
                      return GetIntKwarg(kwargs, "atomic", static_cast<int>(AtomicType::kNone)) ==
                                     static_cast<int>(AtomicType::kNone)
                                 ? ArgEffect::Write
                                 : ArgEffect::ReadWrite;
                    })
    .set_write_channel(WriteChannel::Dma)

"已声明为只读"不等于"未分类"。 HasDeclaredArgEffects() 区分"有人判定该算子不 写入任何参数"(no_arg_writes(),例如 pld.system.wait)与"还没有人看过这个算子"。 需要答案的分析因而可以拒绝猜测,而不是把未分类的写者默认成只读。

强制约束。 OpRegistry::ValidateArgEffects() 在 import 时运行,拒绝两种形态, 一次性列出所有违规算子和修复方式,而不是等到首次使用才失败:

  • 声明了 set_output_reuses_input(N)(其 SSA 结果就是第 N 个参数的缓冲区,因此会 通过它写入),却没有专门对第 N 个参数作出裁决。要求的是"做出分类"而非某个特定答案: 原地槽位是元数据的算子可以声明为只读。
  • 声明了写通路却不通过任何参数写入。通路描述的是"怎么写",因此没有写的通路要么是多余 声明,要么是漏了声明。

第二条比看上去重要。set_write_channel() 会顺带创建效应 spec,因此"spec 存在"不能 等同于"有人做过判断"——否则一个声明了通路却忘了 set_arg_effect 的算子会通过第一条 检查,而它原地更新的那个参数仍然默认为 Readno_arg_writes() 显式记录"对所有参数 的裁决",且与 set_arg_effect 同时使用会被判为自相矛盾而拒绝。

set_write_channel 记录写入走的是 MTE3/DMA 通路还是标量 D-cache 通路。PyPTO 无法为 同一个 GM tensor 排序这两者,因此会拒绝在同一缓冲区上混用两者的函数;有了通路声明, 该诊断可以查询注册表而不必再列一遍算子清单。

只为写入确实走这两条通路之一的算子声明它,其余一律留空——留空会把该算子排除在该诊断 之外,而当两条通路都无法描述它时,这正是它该待的位置:

  • pld.system.notify 发出的是 pto.comm.tnotify,一条独立的 comm 指令。声明任一通路 都会让该诊断拒绝合法程序。
  • system.set_ffts 是把 workspace 指针交给 FFTS 单元,而非搬运数据;该区域由硬件按 自己的节奏写入,没有任何依赖边能建模它。它声明 no_arg_writes()
  • 复合集合通信通过不同机制更新数据窗口与 signal,单个算子级通路无法同时描述两者。 按参数记录通路可以做到,但目前没有任何用例需要这种区分,而一个错误的单一答案比没有 答案更糟。

set_core_affinityset_no_duplicate —— 两个正交的维度,选错会对 ISA 做出错误的断言:

  • set_core_affinity(...) 回答算子在哪个核上运行。只有当硬件确实把算子限制在 某一侧时才声明。未声明时,ClassifyCallAffinity 从调用本身推导放置位置 (先看 memory spec,再看第一个 tile 参数的内存空间),最终回退到 SHARED
  • set_no_duplicate() 回答算子是否允许在第二个核上运行。ExpandMixedKernel 会把 SHARED 语句复制到 AIC 和 AIV 两条通路上;当这份副本会改变程序语义时,标记该算子。

pld.system.notify 是典型例子,而其风险在于从错误的通路上提前释放,而非不幂等: AIC 上的那份副本可能在 AIV 通路的 TPUT 尚未把该信号所释放的数据落盘之前就发布信号, 于是对端 rank 读到过期数据。正因如此该标记是无条件的 —— 尽管只有原子加会重复计数, NotifyOp::kSet 触发的竞态与原子加完全相同。

兄弟算子 pld.system.wait 不标记,且是有意为之:TWAIT 会阻塞,它出现在 cube 通路上是有实际作用的。把它钉在向量通路上,会让 matmul 越过它本应等待的对端数据。 请把该标记读作「不得在第二个核上运行」,而不是「不幂等」。

读取该维度的查询是 IsNoDuplicate()。它唯一的消费者是 LowerAutoVectorSplit (pass 23)的 pl.split_aiv 区域放置标记:该 pass 恰好把区域内的 no-duplicate 调用 钉在 AIV 通路上。没有任何 verifier 在这个维度上做拒绝。

set_core_affinity(...) 固定在单条通路上的算子本来就不会被复制,因此无需该标记。 这个标记正是为核无关(core-agnostic)的算子准备的 —— 对它们而言,没有任何 affinity 取值能表达「两个核都能跑,但不能两个核都跑」。请注意这是关于的断言,而不是关于 总执行次数的断言:在 dual_aiv_dispatch 下,AIV 函数体仍然会在两条 AIV sub-lane 上都 运行,因此把算子挡在 cube 通路之外并不意味着它只执行一次。那一部分属于作者的职责, 文档见作用域 → pl.split_aiv

类型推导签名:

std::function<TypePtr(const std::vector<ExprPtr>& args,
                      const std::vector<std::pair<std::string, std::any>>& kwargs)>

C++ 注册示例

简单逐元素算子

// src/ir/op/tensor_ops/elementwise.cpp
REGISTER_OP("tensor.add")
    .set_op_category("TensorOp")
    .add_argument("lhs", "Left tensor")
    .add_argument("rhs", "Right tensor")
    .f_deduce_type([](const std::vector<ExprPtr>& args,
                      const std::vector<std::pair<std::string, std::any>>& kwargs) {
      CHECK(args.size() == 2);
      auto t1 = std::dynamic_pointer_cast<const TensorType>(args[0]->GetType());
      auto t2 = std::dynamic_pointer_cast<const TensorType>(args[1]->GetType());
      auto dtype = PromoteDataTypes(t1->dtype_, t2->dtype_);
      auto shape = BroadcastShapes(t1->shape_, t2->shape_);
      return std::make_shared<TensorType>(shape.shape, *dtype);
    });

带 Kwargs 的算子

// src/ir/op/tensor_ops/matmul.cpp
TypePtr DeduceMatMul(const std::vector<ExprPtr>& args,
                     const std::vector<std::pair<std::string, std::any>>& kwargs) {
  auto lhs = std::dynamic_pointer_cast<const TensorType>(args[0]->GetType());
  auto rhs = std::dynamic_pointer_cast<const TensorType>(args[1]->GetType());

  auto get = [&](const std::string& k, bool d) {
    for (const auto& [name, val] : kwargs)
      if (name == k) return std::any_cast<bool>(val);
    return d;
  };

  DataType dtype = [&]() {
    for (const auto& [k, v] : kwargs)
      if (k == "out_dtype") return static_cast<DataType>(std::any_cast<int>(v));
    return *PromoteDataTypes(lhs->dtype_, rhs->dtype_);
  }();

  bool a_t = get("a_trans", false), b_t = get("b_trans", false);
  ExprPtr m = a_t ? lhs->shape_[1] : lhs->shape_[0];
  ExprPtr n = b_t ? rhs->shape_[0] : rhs->shape_[1];
  return std::make_shared<TensorType>(std::vector<ExprPtr>{m, n}, dtype);
}

REGISTER_OP("tensor.matmul")
    .set_op_category("TensorOp")
    .add_argument("lhs", "Left matrix")
    .add_argument("rhs", "Right matrix")
    .set_attr<DataType>("out_dtype")
    .set_attr<bool>("a_trans")
    .set_attr<bool>("b_trans")
    .f_deduce_type(DeduceMatMul);

对于二维 tile.matmul,物理装箱后的 K 维必须一致。PTO 从 lhs 的有效 K 推导收缩范围, 因此该范围可以小于 rhs 的有效 K,但必须被后者包含。tile.matmul_acc 同样要求物理 M/N/K 装箱严格兼容,同时允许累加器的有效 M/N 矩形以及 rhs 的有效 K 包含 PTO 根据 lhs M/K 与 rhs N 实际计算的较小矩形。

条件式累加器初始化(init_cond

tile.matmul_acctile.batch_matmul_acctensor.matmul_acctile.gemv_acc 接受一个可选的第四操作数 init_cond:一个 BOOL 标量,用于逐次执行地选择累加器是被 lhs @ rhs 覆写还是被累加。这就是 split-K 的 k == 0 惯用法,它同时省去了清零 累加器与剥离首个 K 步的需要:

acc = pl.tile.create([16, N], pl.INT32, target_memory=pl.Mem.Acc)
for k0 in pl.pipeline(0, K, K_TILE, stage=2):
    ...
    acc = pl.tile.matmul_acc(acc, a_left, b_right, init_cond=(k0 == 0))

该谓词的适用范围与 matmul_acc 本身完全一致:凡是不带谓词能够累加的操作数形状, 带谓词同样能够累加。操作数 rank > 2 的 tensor.matmul_acc 会转换为 tile.batch_matmul_acc,后者将 init_cond 原样转发给 FlattenTileNdTo2D 展开出的 每一个 2D tile.matmul_acc —— 其中每一个都是自己那条累加器行带的唯一写者,因此谓词 按行带逐一生效。(目前只有 batch_count == 1 能走到 codegen;更大的 batch 会在 FlattenTileNdTo2D 中被拒绝,原因与谓词无关 —— 逐 batch 的累加器会是一个跨步的 L0C 行窗口,而 MAD 无法寻址它。)

该谓词是位置操作数而非 registry kwarg,因为它可能依赖循环变量,而 kwarg 只承载 编译期常量。作为操作数注册也意味着它像其他 SSA 值一样参与 use-def 链。

既然是操作数,它在 tile 层按位置打印 —— pl.tile.matmul_acc(acc, lhs, rhs, k0 == 0)。有两个签名的第 4 个位置槽已被占用 (tensor 层是 a_trans,GEMV 是 acc_phase),因此 printer 对它们改用关键字形式打印, init_cond 在这两个 DSL 签名中也相应地是 keyword-only。各形式重新解析后仍是同一份 IR:

pl.tensor.matmul_acc(acc, lhs, rhs, init_cond=k0 == 0, a_trans=False, b_trans=False) pl.tile.gemv_acc(acc, lhs, rhs, init_cond=k0 == 0, acc_phase=pl.AccPhase.Unspecified)

降级方式取决于谓词是否在编译期已知:

init_cond 生成代码
缺省,或字面量 False pto.tmatmul.acc ins(dst, lhs, rhs) outs(dst)
字面量 True pto.tmatmul ins(lhs, rhs) outs(dst)
运行期谓词 scf.if cond { pto.tmatmul } else { pto.tmatmul.acc }

tile.gemv_acc 走同一个 emitter,只是把指令换成 pto.tgemv.acc / pto.tgemv —— GEMV 就是 M 为 1 的 matmul,跑在同一个 cube MAD 上,因此携带同一个 cmatrixInit 位。其 acc_phase 属性会附着在实际生成的那一条指令上。

ISA 将该语义承载为 MAD 指令 Xt 寄存器的第 63 位(cmatrixInit),因此硬件本身 无需分支;分支的来源是 pto.tmatmulpto.tmatmul.acc 是两个独立算子、且不带 init 操作数。由于 matmul_acc 是原地操作(set_output_reuses_input(0)),两个分 支写入同一缓冲区,scf.if 不产生返回值 —— Acc tile 上不会生成 phi。

「字面量」涵盖常量谓词到达 emitter 时的两种形态:DSL 写法 init_cond=True/ False 到达时是 BOOL 类型的 ConstInt,而被更早的 pass 折叠过的谓词到达时是 ConstBool —— 当 LowerPipelineLoops 复制 K-loop 外层循环被消除、每个副本的索引成为字面量时,生成的 ko == 0 正是 这种形态。两者都会直接选定一个分支;若 emitter 只折叠其中一种,未覆盖到的每个 K block 都会发出双倍 MAD。

因此该折叠取决于 trip count,并非普遍成立:在 16x512x64 下流水循环被完全消除, 最终 PTO 中没有 scf.if;而在 16x2048x64 下副本索引仍是符号量(koko + 256),会残留两个 scf.if。这不是回退 —— 它所替换的剥离式 IfStmt 在这些 形状下同样产生两个分支。

编译器自己也使用它所推荐的写法:AutoTileMatmulL0 对普通 tile.matmul 的 K-loop 直接生成带谓词的形式,因此 tile.create 种子、循环携带值与循环的 return_var 在构造上共用同一块 L0C buffer。tile.matmul_bias 没有 init_cond 操作数,无法使用 带谓词的循环体;因此改为把它的第一个 K block 提到循环之外(head-peel):该 block 恰好只加一次 bias 并铸造出累加器,其余 block 统一累加进去。这样无需谓词也能得到同样 的单 buffer 链,因此该 pass 不再生成任何累加器 phi。

一项限制,以显式诊断而非静默丢弃的方式处理:

  • 拒绝 batch_count > 1。这与谓词无关 —— 该形状在不带谓词时同样失败。 FlattenTileNdTo2D 会为每个 batch 取一份累加器的 tile.slice,而多 block 列 L0C tile 的行窗口是跨步的,MAD 无法寻址(pto-isa#253)。只要 batch 维之积 为 1,rank > 2 就是允许的 —— 这正是 grouped GEMM 的情形([1, N, K] 权重)。 若确实需要多个 batch,请改为在 batch 维上循环。

超尺寸的带谓词 tile.matmul_acc 与无谓词形式一样会被做 K 切分:调用方的谓词与 所生成循环自身的 ko == 0 做与运算,而剥离出的尾块保持无谓词的 3 操作数形式 (它永远不是第一个 K block)。

累加形式的 M/N 切分只在循环层可用,且两种写法一视同仁:由 tile.create([M, N])、split-K pl.pipeline 与单个 2D store 组成的三元组会在其 K-loop 之外被切分,无论其中的归约是 peel 写法 (if ko == 0: matmul else: matmul_acc)还是谓词写法 (matmul_acc(acc, lhs, rhs, ko == 0))。不属于该形态时 —— 独立的超尺寸 tile.matmul_acc(累加器由调用方持有),或 init_cond 不是对循环归纳变量首块判定的 带谓词调用 —— 切分累加器不受支持,该 pass 会以 PH-AT-006 性能提示明确说明。

在 tile 层,tile.batch_matmulTileType 操作数提供批量语义。它接受 rank >= 2 的 tile,广播前导批量维度,并保持与 tile.matmul 相同的纯操作数接口风格。如果批量操作数 需要转置语义,可以通过两种等价方式表达:在输入上显式使用 tile.transpose(...),或在 自然 tile.load 上叠加零拷贝 tile.transpose_view(...)。在后续降级到 2D tile.matmul 时,这两种写法都会被统一识别为操作数转置语义。

tile.batch_matmul_acc(acc, lhs, rhs) 是批量路径上的累加版本:acc = acc + lhs @ rhs, 遵循与 tile.batch_matmul 一致的 rank>=2 + batch 广播规则。acc 的 batch 形状必须与 lhs/rhs 广播后的 batch 形状完全一致;matmul 的 (M, N) 必须与 acc 的末两维一致;K 维必须 与 lhs/rhs 内层匹配。累加器的内部 dtype 默认为浮点 → FP32、整型 → INT32(与 tile.matmul_acc 对齐)。在 conversion 阶段,ConvertTensorToTileOps 会把 tensor.matmul / tensor.matmul_acc 在任一操作数 rank > 2 时分派到该批量路径;后续由 FlattenTileNdTo2D 将其展开为逐 batch 的 2D 操作。

MX block-scale matmul(Ascend950)

MX 使用独立的 LeftScale / RightScale 内存空间与 FP8E8M0 scale dtype。PyPTO 在 Ascend950 上通过 matmul_mx 算子族支持 host-prequant MXFP8,以及显式转换后的 FP4×FP8 路径。 InsertMxScaleAddr(在 InferTileMemorySpace 之后)在操作数内存空间解析完成后插入内部 tile.tget_scale_addr 绑定。

IR / DSL 说明
tile.load 读取 pl.Tensor[..., pl.MX_A_ZZ \| pl.MX_B_NN] 源 TensorLayout 携带 MX scale GM layout。dtype 为 FP8E8M0,且不支持 strided source。公开 pl.load 在省略 target 时默认为 Mat;原始 IR 必须携带 target_memory=Mat
tile.move(..., target_memory=LeftScale/RightScale) Mat→Scale move;硬件 layout 固定为左侧 row/row/32、右侧 col/col/32,源 Mat tile 与 layout override 必须完全匹配。
tile.create(..., target_memory=LeftScale/RightScale) 不支持;应先把 MX scale 数据加载到 Mat,再 move 到 scale 内存。
tile.matmul_mx / pl.matmul_mx Left, LeftScale, Right, RightScale → Acc;操作数位置驱动自动放置,包括为 quant_mx scale 生成 Vec→Mat→LeftScale/RightScale staging。进入算子的两块 data operand 必须都是 FP8E4M3FN,scale 为 FP8E8M0lhs_scalerhs_scale 必须是不同的 tile。支持的 FP4 输入形式仅为左侧 FP4×右侧 FP8,且必须先显式写 pl.cast(fp4, pl.FP8E4M3FN);原生 FP4×FP4 与反向 FP8×FP4 会被拒绝。Physical M/K/N、valid K 与 scale-group 数均以 cast 后进入算子的 FP8 tile extent 为准,不使用 packed x2 carrier shape。Physical M % 16 == 0K % 64 == 0N % 32 == 0;valid K 必须满足 ceil(validK/32) == ceil(physicalK/32)。对齐与 scale-group 数值检查仅作用于常量维;符号维跳过数值校验,回退到声明的 scale tile 几何(后续仍由 PTOAS 验证)。
tile.matmul_mx_acc / pl.matmul_mx_acc Acc, Left, LeftScale, Right, RightScale → Acc;通过 set_output_reuses_input(0) 原地执行;accumulator 的 physical/valid M、N 必须与 matmul 输出一致。
tile.matmul_mx_bias / pl.matmul_mx_bias Left, LeftScale, Right, RightScale, Bias → Acc;bias 为 [1, N] FP32。
tile.tget_scale_addr 编译器生成的 A5 绑定,接受 LeftScale↔LeftRightScale↔Right;对 dst_scale 原地 DPS。用户只编写 matmul_mx 算子族。

规范样例:M=128,K=64,N=64,进入算子的 A/B 均为 FP8E4M3FN,scale=FP8E8M0[128,2] / [2,64]), GM scale layout mx_a_zz / mx_b_nn(host ZZ/NN pack)。左侧输入可来自 FP4,但必须先显式转为 FP8;对齐 M↑16、K↑64、N↑32。

MX tensor subview 是当前遗留限制。由于硬件路径无法表达 subview base offset,tensor.slicetensor.reshapetensor.transposetensor.reinterpret_view 以及普通的 MX tensor.view 均拒绝 MX-layout source。 唯一例外是 packed ND backing 与 MX_A_ZZ / MX_B_NN 之间、元素总数不变的 FP8E8M0 shaped alias(用于 GM 分核暂存)。在完整的 scale layout contract 实现前, pld.tile.remote_load 也拒绝 MX layout。tensor.gather_row / tile.gather_row 同样拒绝 MX source。

FP4 Tensor/Tile shape 与 valid_shape 都以逻辑 nibble 计数;末维必须是正偶数,slice 的线性起点也不能落在一个字节的第二个 nibble。Torch/runtime 继续以物理 x2 shape 携带 float4_e2m1fn_x2,JIT/compiled-call 边界负责换算,因此 IR 不增加持久化 storage_shape

A5 会把左侧显式 FP4→FP8 tile cast 展开为 FP4→BF16→FP32→FP8E4M3FN。它是 data operand 的数值 cast,不修改 scale。原生 packed-FP4 矩阵乘仍不支持;独立 MXFP4 量化不在本版本范围内(pl.quant_mx 仅 MXFP8)。

MX / Ascend950:pto-isa 约束

约束 要点
独立 scale buffer Cube 把 scale 折进 Left/Right data;TileType::ScaleLeft / ScaleRight(L0A/L0B sidecar)↔ PyPTO LeftScale / RightScale
payload scale 为 float8_e8m0_t / FP8E8M0;实际发射的 MX data pair 为 FP8E4M3FN × FP8E4M3FN(拒绝 FP8E5M2 与原生 packed FP4)。逻辑 FP4×FP8 输入先把左侧 FP4 转为 FP8;physical K、valid K 与 ceil(K/32) scale 组数均以 cast 后的 FP8 tile 为准,而非 packed x2 carrier;physical K%64==0,fractal=32
layout mx_a_zz → row-major ZZ;mx_b_nn → col-major NN;TLoadMxCube*(AZZ2ZZ 等)
TMov CommonCheckMX 允许 uint8_t Mat → float8_e8m0 ScaleLeft/Right;canonical:ui8 Mat reshape 再 ui8→f8 Scale
bind-then-fill GetScaleAddr(Left/Right) 再填 sidecar;写 provisional alloc 地址在 rebound 后无效
对齐 cast 后的 FP8 tile extent 要求 physical M%16==0K%64==0N%32==0DeduceTileMatMulMxType 仅对常量维强制;符号维跳过数值检查

MX / Ascend950:PTOAS 约束

约束 要点
单一 loc=scaling 尚无独立 left/right_scale loc;PyPTO 两侧都降到 loc=scaling,EmitC 再选 ScaleLeft/Right
dtype 必须 !pto.f8E8M0 ui8+scaling 会错成 Fixpipe TileType::Scaling;进 Scale 前需提升为 FP8E8M0
禁止 Mat↔Scaling treshape 不同 loc;reshape 留在 Mat(ui8),再 tmov 进 scaling
shape-matched Mat→Scale tmov flat [1,G] 须先 treshape[M,K/32](或 B 侧 shape)
顺序 PyPTO 按源序发 Mat→scaling tmov;PTOAS PTOA5NormalizeTMovPasstget_scale_addr 重排到它前面(ISA bind-before-fill)
#pto.layout / mx load mx_a_zz / mx_b_nn / …;codegen 发射逻辑 rank-2 make_tensor_view(PTOAS v0.60 InferPTOLayout / EmitC 映射物理 pack)
本阶段覆盖 pto.tmatmul.mx / .acc / .bias + pto.tget_scale_addrpto.tquant.mxLowerCompositeOps

仅 Tile 的 GEMV 家族(A2/A3)

仅 tile 的 GEMV 家族逻辑形状为 [1, N],但物理形状遵循 Cube 指令的对齐契约: Acc 结果使用 16 个物理行,物理列数沿用 RHS tile(并须满足目标平台通常的 C0 对齐要求),bias 使用相同的物理列数; 各自的 valid_shape 仍保留逻辑 [K, N][1, N][1, N] 区域。 lhs 的物理行数和逻辑行数都必须恰好为 1。 单行 Mat load 使用 blayout=row_majorslayout=none_box,从而选择 PTO-ISA 的行向量提取路径。

rhs 的逻辑 K 必须覆盖 lhs 的逻辑 K。支持的 dtype 三元组为 INT8 x INT8 -> INT32,以及同类型 FP16BF16FP32 输入到 FP32gemv_accacc 使用对应输出 dtype,gemv_biasbias 也必须使用相同的输出 dtype,且 bias 的 valid shape 必须覆盖逻辑输出 [1, N];物理 N 一致时,bias 的 valid N 可以更宽。

tile.gemvtile.gemv_acctile.gemv_biasacc_phase 可设为 pl.AccPhase.Unspecified(默认值)、pl.AccPhase.Partialpl.AccPhase.Final。后续仍有 K 分块时使用 Partial,最后一个分块使用 Final

tile.gemv_acc 还接受可选的 init_cond 谓词 —— 见条件式累加器初始化tile.gemv_bias 没有该操作数, 与 tile.matmul_bias 一致:带 bias 的 GEMV 本身就铸造累加器,没有可被谓词化的初值。

Acc 的补齐契约决定了带谓词的 split-K GEMV 如何铸造该累加器。由于 [1, N] 结果占用 16 个物理行,pl.tile.create([1, N], ...) 会因物理 shape 被拒,[16, N] 则因 valid shape 被拒;应按物理 shape 创建再收窄 valid 矩形:

acc_raw = pl.tile.create([16, N], pl.FP32, target_memory=pl.Mem.Acc)
acc = pl.tile.set_validshape(acc_raw, 1, N)  # 随后 gemv_acc(..., init_cond=(k0 == 0))

init_cond 之前,这一步是由剥离的首个 K 步隐式完成的 —— 一条直线展开的 pl.tile.gemv 会铸造出类型正确的累加器,代价是两个分支之间的一个 phi。

在使用 unit flag 的路径上,最后一个累加生产者必须与 pl.store(..., st_phase=pl.STPhase.Final) 配对。final 生产者负责置位,final store 负责检查并清位;普通 store 则有意保留默认的 pl.STPhase.Unspecified 行为。PTO-ISA 的 check-only store phase 需要有序的 多消费者生命周期,因此 PyPTO 不对外提供该阶段。编译器会双向校验所支持的 final 配对:应绑定 final 生产者的结果,并在同一个直线控制流区域内存储这个 精确值。缺失或错配的配对会在代码生成前被拒绝,因为它原本会导致设备静默挂死。

Python 用法

from pypto.pypto_core import DataType, ir
from pypto.ir import op

span = ir.Span.unknown()
dim4, dim8 = ir.ConstInt(4, DataType.INT32, span), ir.ConstInt(8, DataType.INT32, span)

# Create tensors
tensor_a = ir.Var("a", ir.TensorType([dim4, dim8], DataType.FP32), span)
tensor_b = ir.Var("b", ir.TensorType([dim8], DataType.FP32), span)

# Simple operators
result = op.tensor.add(tensor_a, tensor_b)  # Broadcasting: [4,8] + [8] → [4,8]

# Operators with kwargs
dim64, dim128 = ir.ConstInt(64, DataType.INT32, span), ir.ConstInt(128, DataType.INT32, span)
a = ir.Var("a", ir.TensorType([dim64, dim128], DataType.FP16), span)
b = ir.Var("b", ir.TensorType([dim128, dim64], DataType.FP16), span)
matmul = op.tensor.matmul(a, b, out_dtype=DataType.FP32, a_trans=True)

# Query registry
assert ir.is_op_registered("tensor.add")
op_instance = ir.get_op("tensor.add")

Kwargs(关键字参数)

Call 表达式 (Expression) 将 Expr 参数与元数据参数通过 kwargs 分离。

Kwargs vs Args vs 属性 (Property)

- Args Kwargs Op 属性
类型 ExprPtr std::any 类型擦除
作用域 每次调用 每次调用 全局
用途 张量、维度、偏移 out_dtype、标志、模式 设备、分类
访问方式 call.args_ call.kwargs_ op.get_attr()

C++ - 读取 Kwargs

TypePtr DeduceCastType(const std::vector<ExprPtr>& args,
                       const std::vector<std::pair<std::string, std::any>>& kwargs) {
  auto input = std::dynamic_pointer_cast<const TensorType>(args[0]->GetType());

  // `kwargs` is a vector of pairs, not a map — scan it to look a key up.
  auto find_kwarg = [&kwargs](const std::string& key) {
    return std::find_if(kwargs.begin(), kwargs.end(),
                        [&key](const auto& kv) { return kv.first == key; });
  };

  // Required kwargs — `cast` declares both `target_type` and `mode`, and codegen
  // reads `mode` unconditionally, so a missing one must fail here rather than
  // silently default to round_mode NONE.
  auto it = find_kwarg("target_type");
  CHECK(it != kwargs.end()) << "tensor.cast requires 'target_type'";
  DataType target = static_cast<DataType>(std::any_cast<int>(it->second));

  CHECK(find_kwarg("mode") != kwargs.end()) << "tensor.cast requires 'mode'";

  return std::make_shared<TensorType>(input->shape_, target);
}

真正可选的 kwarg(codegen 读取时带回退值,例如 tile.loghigh_precision)应使用 Call::GetKwarg<T>(key, default_value) 读取,而不是 CHECK——参见 include/pypto/ir/expr.h

Python - 使用 Kwargs

result = op.tensor.matmul(a, b, out_dtype=DataType.FP32, a_trans=True)
print(result.kwargs)  # {'out_dtype': 51, 'a_trans': True}

广播与类型提升

NumPy 风格广播

维度从右向左对齐:

[4, 8] + [4, 8] → [4, 8]  # Exact match
[4, 8] + [8]    → [4, 8]  # Missing left dimension = 1
[4, 1] + [8]    → [4, 8]  # Size 1 broadcasts
[1, 8] + [4, 8] → [4, 8]  # Size 1 broadcasts
[4, 8] + [5]    → Error   # 8 ≠ 5

类型提升

标准数值规则:浮点 > 整数,大尺寸 > 小尺寸,有符号 > 无符号(相同大小时)。

INT32 + INT32 → INT32
INT32 + FP32  → FP32   (float precedence)
INT32 + INT64 → INT64  (larger size)
UINT32 + INT32 → INT32 (signed precedence)

Tensor 与 Tile 算子

数据算子 API、扁平 gather、有效区域语义、Tile 布局与掩码模式,参见 Tensor 与 Tile 算子

SyncOp:同步操作

用途:硬件同步与屏障,以及共用 system. 命名空间的 TaskId 与 SPMD 启动形状查询 类型:屏障类为 UnknownType(无返回值,在 EvalStmt 中使用);查询类为 ScalarType,会绑定一个值(task_invalidtask_is_validavailable_cluster_countavailable_aiv_count位置src/ir/op/sync_ops/ —— sync.cpp(屏障)、task.cpp(TaskId)、launch.cpp(启动形状查询) Python APIfrom pypto.ir.op import system

操作 描述 Kwargs
system.bar_all 全局屏障(下降为 pto.barrier <PIPE_ALL>
system.bar_v 向量屏障(下降为 pto.barrier <PIPE_V>
system.bar_m 矩阵屏障(下降为 pto.barrier <PIPE_M>
system.fence 全局内存屏障(下降为 pto.fence.barrier_all #pto.fence_scope<gm>
system.cacheinvalid 使 tensor 子区域基地址所在的那一条 cache line 失效。参数:tensorshapes(N 维)、offsets(N 维)。任意区域大小(包括单个元素)都下降为 pto.partition_view + pto.cmo.cacheinvalid %payload_view single_cache_line : !pto.partition_tensor_view<...>shapes 不会让它遍历区域内的所有 cache line。无参数形式使全部 GM cache 失效。
system.syncall 跨核全员屏障(pto::SYNCALL)。属性 mode"hard"(FFTS,无 operand)或 "soft"(GM 轮询,带 operand) core_type"aiv_only" | "aic_only" | "mix")、mode"hard" | "soft"
system.sync_src 设置同步标志 set_pipe, wait_pipe, event_id
system.sync_dst 等待同步标志 set_pipe, wait_pipe, event_id
system.task_invalid TaskId::invalid() 哨兵——TaskId carry 的 "暂无 producer" 种子
system.task_is_valid 测试某个 TASK_ID 值是否为有效(非哨兵)handle 无;唯一位置参数是 TaskId Var
system.available_cluster_count 本次运行的 MIX cluster(= AIC)数,由设备读回。结果为 Scalar[INT32]
system.available_aiv_count 本次运行的独立 AIV 核数,由设备读回。结果为 Scalar[INT32]

system.syncall 有两种 mode,由其 mode IR 属性选择;Python 接口则用 pl.SyncAllMode 成员表达(见下文)。hard 形态(属性 "hard",默认)下沉为 FFTS 屏障,等待所选 core_type全部物理核到达;kernel 必须以满占用方式启动(每个物理核一个 block)且带 sync_start=True(使所有 block 同时驻留——非 sync_start 启动可能分波次派发 block 而使屏障死锁),否则屏障死锁(AICore 错误 507018)。soft 形态(属性 "soft")轮询一段共享 GM workspace,因此可在部分占用下工作。gm_workspace 是共享、清零的 GM INT32 tensor,至少包含 16 个元素(64 字节)。请将它作为 kernel 参数传入,使所有 block 共享同一缓冲;该缓冲必须独占一条 cache line,并在首次使用前清零。

当前 PTO-ISA 对所有 core_type 使用相同的 soft operand ABI:[gm_workspace] 从设备启动配置推导参与核数,[gm_workspace, used_cores] 则以 INT32 范围内的 Python 整数或 INT32 标量显式指定。高层 DSL 要求必须传入 used_cores 以明确选择:正数生成双 operand 形式,显式传入 0 才生成单 operand 形式。对 mix 而言,显式计数是 AIC 与 AIV 参与者的总数。当 runtime 的逻辑 grid 与设备启动寄存器不一致时必须传入正数;当前 PyPTO 固定的 Simpler runtime 就属于这种情况。不再需要 UB/L1 scratch tile。

两种 mode 都只保证 barrier 到达:不会等待 TSTORE 等前序数据指令,也不会发布或使业务数据的 cache line 失效。跨核通过 GM 交接可能跨多条 cache line 的数据时,应保守地在 barrier 前用全 GM system.cacheinvalid()system.fence 显式发布 producer 的写,然后在 consumer 读之前用全 GM system.cacheinvalid() 使其 cache 失效。tensor-region 形式只使 view 基地址所在的那一条 cache line 失效。

core_typemode 属性在 IR 中仍是字符串,但 Python 接口是枚举:pl.KernelTypeAIC / AIV / MIX,表示算子属于展开后的哪个 kernel)与 pl.SyncAllModeHARD / SOFT)。只接受枚举成员:下沉后的属性拼写是 API 的产物而非输入,传字符串抛 TypeError;传了不属于该算子取值域的成员抛 ValueError。统一的 mode= 关键字 API 是 DSL 层接口(pl.system.syncall)。pypto.ir.op.system 下的 Python IR 辅助函数则是拆开的:syncall(core_type=...) 构造 hard 形态,syncall_soft(core_type, gm_workspace, used_cores=None) 构造 soft 形态。

system.available_cluster_count / system.available_aiv_count 是 SPMD 启动形状查询:把它作为 pl.spmd(...)core_num 传入,启动宽度即按本次运行落到的设备自适应。Orchestration codegen 分别下沉为 rt_available_cluster_count() / rt_available_aiv_count()。混合(AIC+AIV)或纯 cube kernel 用 cluster 数(每个 core-group 一个 block),纯 vector kernel 用 AIV 数。这是唯一能跨设备保持满占用的启动宽度,而 hard system.syncall 正需要满占用;HardSyncallOccupancy verifier 对这类宽度不再做数量比较,并会拒绝用错核类型的查询。请把调用内联传入(pl.spmd(pl.system.available_cluster_count())),不要先绑定到变量名——变量名会以「定义在调用方的变量」形式落到外提出的 Spmd 包装函数上,IR printer 无法重新解析。源码:src/ir/op/sync_ops/launch.cpp

system.task_invalid 返回类型为 ScalarType(DataType::TASK_ID)。当 Python 字面量 None 出现在 TaskId 位置(deps=[None] 条目或 TaskId 循环 iter_arg 种子)时,它就是 Nonewith pl.manual_scope(): 区域内的下沉目标。不存在 system.task_id_of op —— producer task id 由 pl.submit(...) parser construct 返回的二元组第二个元素获得,而非来自 builtin。源码:src/ir/op/sync_ops/task.cpp

CrossCoreOp:AIC↔AIV 跨核通信

用途:AIC (Cube) 和 AIV (Vector) 内核之间的跨核同步、数据传输和管道管理 类型UnknownType(sync/push/init/buffer/free 操作)或 TileType 透传(pop 操作) 位置src/ir/op/tile_ops/cross_core.cpp(tpush/tpop)和 src/ir/op/sync_ops/cross_core.cpp(sync/tfree/管道初始化/缓冲区) Python APIimport pypto.language as pl(提升的操作)或 from pypto.ir.op import tile, system

显式事件同步

操作 参数 描述 Kwargs
system.sync_set 0 或 1(event_id_dyn 从一种核类型发出 pto.sync.set pipe、静态 event_id、可选 ffts_mode、可选 core_type
system.sync_wait 0 或 1(event_id_dyn 在对端核类型发出 pto.sync.wait pipe、静态 event_id、可选 core_type
system.set_ffts 1(workspace 声明 A3 显式跨核事件所需的 FFTS 设置

在显式指定类型的 AIC/AIV kernel 中使用 pl.system.sync_set(event_id, pipe=..., ffts_mode=...)pl.system.sync_wait(event_id, pipe=...)。在混合 InCore kernel 中,传入 core_type=pl.KernelType.AIVcore_type=pl.KernelType.AIC,以便 kernel 展开时将各事件操作保留在目标核通道上(IR 属性仍保存下沉后的 "aiv" / "aic" 拼写,那是 API 的产物,不是可接受的输入)。这里不接受 pl.KernelType.MIX——事件只钉一条 lane,两条都跑是通过省略 core_type 表达的。system.syncall 与事件算子最终都归入 ClassifyCallAffinity 的同一套 KernelType 分类,区别只在 IR 属性的拼写("aic_only""aic")。在 A3 上,每个参与同步的 AIC/AIV 函数都必须在首次显式事件操作前调用 pl.system.set_ffts(workspace)workspace 必须是至少包含 256 个元素的一维 INT64 张量,并作为 PTOAS 的设置操作数。PyPTO 的常驻运行时会持续安装硬件 FFTS 控制地址,因此生成的运行时封装不会用该操作数覆盖此地址。A5 不需要该设置。event_id 可以是用户可用范围 0–13 内的整数,也可以是动态 pl.Scalar[pl.INDEX];ID 14 和 15 为保留值。sync_set 的可选 ffts_mode 必须为 0、1 或 2。手写跨核协议的作者负责正确配对事件 ID 和 pipe。PyPTO 的常规核内自动依赖插入仍保持启用,并使用独立的 set_flag/wait_flag 机制,因此不会占用这些显式跨核事件 ID。

数据传输操作

操作 参数 描述 Kwargs
tile.tpush_to_aiv 1 (tile) 从 Cube 推送 tile 到 Vector split,可选 id
tile.tpush_to_aic 1 (tile) 从 Vector 推送 tile 到 Cube split,可选 id
tile.tpop_from_aic 0 从 Cube 管道弹出 tile(→ TileType) split,可选 id
tile.tpop_from_aiv 0 从 Vector 管道弹出 tile(→ TileType) split,可选 id
system.tfree_to_aic 1 (tile) 向 Cube 生产者释放槽位 可选 id
system.tfree_to_aiv 1 (tile) 向 Vector 生产者释放槽位 可选 id

管道初始化操作

操作 参数 描述 Kwargs
system.aic_initialize_pipe 2 在 Cube 侧初始化跨核管道(位置参数:c2v_consumer_bufv2c_consumer_buf,i32 SSA) dir_mask, slot_size,可选 slot_num,可选 local_slot_num,可选 id
system.aiv_initialize_pipe 2 在 Vector 侧初始化跨核管道(位置参数:c2v_consumer_bufv2c_consumer_buf,i32 SSA) dir_mask, slot_size,可选 slot_num,可选 local_slot_num,可选 id
  • slot_num(设置时必须 > 0)显式指定 GM 环形缓冲区的槽数量;省略时由 PTOAS 取默认值(单向 8,双向每方向 4)。
  • local_slot_num(仅 a2/a3,必须 > 0 且 <= slot_num)显式指定本地槽数量。
  • 预留/导入缓冲区大小需由用户自行设置,且与架构相关a3slot_size * local_slot_numa5slot_size * slot_num

缓冲区管理操作

操作 参数 描述 Kwargs
system.reserve_buffer 0 预留跨核通信命名缓冲区(消费者侧) name, size, base*
system.import_peer_buffer 0 从同组对等函数导入缓冲区(生产者侧) name, peer_func

* base 默认为 AUTO (-1),由编译器自动分配地址。

DSL 示例(跨核 V2C 单向)

dir_mask=2 仅启用 V2C,因此 C2V 侧缓冲区实参需为未使用方向的占位(0pl.const(0, pl.INT32));启用侧将 reserve_buffer / import_peer_buffer 的句柄作为第一个位置实参传入。

import pypto.language as pl

@pl.program
class CrossCoreExample:
    @pl.function(type=pl.FunctionType.InCore)
    def vector_producer(self, a: pl.Tensor[[16, 16], pl.FP16]):
        peer = pl.import_peer_buffer(name="v2c_buf", peer_func="cube_consumer")
        pl.aiv_initialize_pipe(pl.const(0, pl.INT32), peer, dir_mask=2, slot_size=512)

        tile_a: pl.Tile[[16, 16], pl.FP16] = pl.load(a, [0, 0], [16, 16])
        pl.tpush_to_aic(tile_a, split=0)

    @pl.function(type=pl.FunctionType.InCore)
    def cube_consumer(self, out: pl.Tensor[[16, 16], pl.FP32]) -> pl.Tensor[[16, 16], pl.FP32]:
        buf = pl.reserve_buffer(name="v2c_buf", size=4096, base=0x1000)
        pl.aic_initialize_pipe(pl.const(0, pl.INT32), buf, dir_mask=2, slot_size=512)

        received: pl.Tile[[16, 16], pl.FP16] = pl.tpop_from_aiv(split=0)
        pl.tfree_to_aiv(received)
        result: pl.Tensor[[16, 16], pl.FP32] = pl.store(received, [0, 0], out)
        return result

参阅 TPUSH/TPOP ISA 参考缓冲区管理了解硬件细节。

PrefetchOp:GM→L2 异步预取

一种隐藏访存延迟 (latency hiding) 的缓存提示。async_prefetch 通过 SDMA 异步地把一段 全局内存 (GM) 拉入 L2 缓存,期间可以并行执行不相关的计算;wait 阻塞直到预取完成。 预取不改变任何张量的值——同一个 kernel 加不加预取在数值上完全一致,只影响性能。

与大多数 PTO intrinsic 不同,TPREFETCH_ASYNC 不携带隐式的 wait-event 同步, 因此必须通过 event/session 这对句柄显式等待完成。

操作

DSL 操作数 结果 PTOAS op
pl.prefetch.make_context() PrefetchAsyncContextType pto.make_prefetch_async_context
pl.prefetch.async_prefetch(src, ctx) GM Tensor、context AsyncEventType pto.tprefetch_async
pl.prefetch.session(ctx) context AsyncSessionType pto.get_prefetch_async_session
pl.prefetch.wait(evt, session) event、session BOOL 标量 pto.comm.wait_async_event

这三个结果类型都是不透明的单例标记类型 (opaque singleton marker,无 shape、无 buffer), 与 CommCtxType 属于同一族。SDMA workspace 不是程序操作数:runtime 持有它, codegen 会向 prefetch kernel 注入隐藏指针。

约束

  • src 必须是扁平连续的逻辑一维 GM 区域:shape 必须完全静态,且除最后一维外 所有维度都为 1[N][1, N][1, 1, N])。该检查与 PTOAS 的 TPrefetchAsyncOp::verify() 保持一致,因此 shape 写错会在 PyPTO IR 构造阶段就报错, 而不是拖到 PTOAS 校验阶段。

使用示例

@pl.program
class PrefetchExample:
    @pl.function(type=pl.FunctionType.InCore)
    def main(
        self, x: pl.Tensor[[1, 4096], pl.FP32],
        out: pl.Tensor[[1, 128], pl.FP32],
    ) -> pl.Tensor[[1, 128], pl.FP32]:
        ctx = pl.prefetch.make_context()
        evt = pl.prefetch.async_prefetch(x, ctx)     # 预热 L2,不阻塞
        session = pl.prefetch.session(ctx)
        # ... 此处的无关计算与预取重叠执行 ...
        pl.prefetch.wait(evt, session)               # 此时 x 已驻留在 L2
        tile = pl.load(x, [0, 0], [1, 128])
        return pl.store(tile, [0, 0], out)

执行核:这一族是 AIV-onlyTPREFETCH_ASYNC 的 SDMA tmpBuf 来自 PrefetchAsyncContext 内部的 Vec(UB) scratch tile(pto-isa 有 static_assert(ScratchTile::Loc == TileType::Vec)),而 UB 位于向量核。这些算子 声明了 CoreAffinity::VECTOR,因此在混合 kernel 中 ExpandMixedKernel 会把它们留在 向量侧——既不会放到 cube 侧,也不会被复制到 cube 侧。

Runtime 所有权与支持范围:普通的单次执行 (one-shot execution) 会读取 生成 artifact 中的 SDMA 需求,并自动创建已启用 SDMA 的 worker。user、 orchestration 和 runtime tensor signature 中都不会出现 workspace。显式复用 L2 worker 时,需在构造时启用该能力:

with ChipWorker(
    config=RunConfig(platform="a2a3", device_id=0), enable_sdma=True
):
    compiled(a, out, config=cfg)

当前由 runtime 提供 workspace 的执行路径仅在 onboard a2a3 上覆盖。在模拟器、 a5 或不提供 SDMA provider 的 runtime 上,启用该能力的 worker 会在 runtime 初始化时失败。PyPTO 不会分配后备 workspace,也不会把请求的 prefetch 静默降级为 no-op。onboard a2a3 ST 参见 tests/st/runtime/ops/test_prefetch_async.py

文件组织

目录/文件 内容
src/ir/op/type_inference.cpp 共享的类型推断工具
tensor_ops/elementwise.cpp TensorOp: add, sub, mul, div
tile_ops/matmul.cpp TileOp:matmul、gemv
tile_ops/matmul_mx.cpp TileOp:matmul_mx、matmul_mx_acc、matmul_mx_bias、内部 tget_scale_addr 绑定
tile_ops/memory.cpp TileOp: load, store, read, get_block_idx
tile_ops/elementwise.cpp TileOp: add, mul, div, adds, muls 等
tile_ops/reduction.cpp TileOp: sum(含 axis, keepdim)
tile_ops/unary.cpp TileOp: sqrt
sync_ops/sync.cpp SyncOp: sync_src, sync_dst, barriers
sync_ops/task.cpp SyncOp:TaskId 哨兵与判定
sync_ops/launch.cpp SyncOp:SPMD 启动形状查询
sync_ops/cross_core.cpp CrossCoreOp: tpush, tpop, pipe init, buffers
prefetch/prefetch_async.cpp PrefetchOp: make_context, async_prefetch, session, wait

优势

  • 模块化:自包含的算子分类
  • 构建性能:修改一个分类不会重新构建其他分类
  • 可维护性:易于定位和修改算子
  • 可扩展性:直接添加新算子

添加新操作

  1. 选择分类文件src/ir/op/tensor_ops/elementwise.cppmatmul.cppreduction.cpp,或 src/ir/op/tile_ops/memory.cppunary.cpp

  2. 实现类型推导

TypePtr DeduceType(const std::vector<ExprPtr>& args,
                   const std::vector<std::pair<std::string, std::any>>& kwargs) {
  CHECK(args.size() == 2) << "op requires 2 arguments";
  // Validate types, read kwargs, compute output type
  return result_type;
}
  1. 注册
REGISTER_OP("tensor.matmul")
    .set_op_category("TensorOp")
    .add_argument("lhs", "Left tensor")
    .add_argument("rhs", "Right tensor")
    .set_attr<DataType>("out_dtype")
    .f_deduce_type(DeduceType);
  1. Python 封装 (python/pypto/ir/op/tensor_ops.py):
def matmul(lhs: Expr, rhs: Expr, out_dtype=None, a_trans=False) -> Call:
    kwargs = {}
    if out_dtype: kwargs["out_dtype"] = out_dtype.code() if isinstance(out_dtype, DataType) else out_dtype
    if a_trans: kwargs["a_trans"] = a_trans
    return _ir_core.create_op_call("tensor.matmul", [lhs, rhs], kwargs, Span.unknown())
  1. 添加测试,位于 tests/ut/ir/,如需要则更新 CMakeLists.txt

要产生多个值? 先读多输出算子——结果属于 TupleType,绝不放进参数列表;凡是这类算子会写、却没有声明为 workspace 的参数,注册表都会在 import 期拒绝。

参考

核心定义位于 include/pypto/core/common.hinclude/pypto/ir/;注册表与类型推断实现在 src/ir/,算子实现按类别位于 src/ir/op/{tensor_ops,tile_ops,sync_ops}/