跳转至

算子系统

旧 Python 接口示例: 本页为兼容现有用法保留 platform= 调用, 这些调用会发出 DeprecationWarning;新代码应使用 backend_type 和 execution_mode。参见目标映射。

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

算子分类

分类 类型 用途 文件位置
TensorOp TensorType 支持广播的 N 维张量 (Tensor) 操作 src/ir/op/tensor_ops/
TileOp TileType 硬件优化的 Tile 操作 src/ir/op/tile_ops/
BufferOp BufferType, VoidType 内部显式存储与目标写入 src/ir/op/buffer_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)

内部 Buffer 阶段的 GM 与加法算子不提供公开 DSL 包装:

算子 位置操作数 结果
buffer.load GM tensor、offsets tuple、valid extents tuple、目标 buffer Void
buffer.store 源 buffer、offsets tuple、valid extents tuple、GM tensor Void
buffer.add lhs buffer、rhs buffer、目标 buffer Void
buffer.copy 源 buffer、目标 buffer(Vec 到 Vec,或 Mat 到 Left/Right) Void
buffer.extract 源 buffer、行偏移、列偏移、目标 buffer Void
buffer.matmul Left buffer、Right buffer、Acc 目标 Void
buffer.matmul_acc Left buffer、Right buffer、被读写的 Acc buffer Void

这些算子分别声明数据/元数据效应。形状、dtype、valid 状态和别名要求见 Buffer 契约。

内部 buffer.get_block_idx、buffer.get_block_num 和 buffer.get_subblock_idx 是无参数的 Buffer 阶段查询,返回 INDEX 标量(BufferResultBehavior.Value),没有执行期内存访问。 它们读取既有运行时传入的 SPMD kernel 参数,由对应 Tile 查询转换产生,不提供公开 DSL 接口。

类型化 Buffer 逐元素配方

backend/common/buffer_elementwise_recipes 是逻辑到 Buffer 转换、显式操作数约束及 原生助记符共用的生产表。LowerTileToBuffer 选择类型化配方 (typed recipe) 并创建 实际的 Buffer 调用。直接 PTO 发射读取同一张表,不重建 Tile IR、不调用旧回调, 也不选择存储位置。此表独立于旧后端回调注册。

逻辑算子 Buffer 算子 原生指令 输入
tile.add buffer.add pto.tadd 2
tile.mul buffer.mul pto.tmul 2
tile.sub buffer.sub pto.tsub 2
tile.div buffer.div pto.tdiv 2
tile.maximum buffer.maximum pto.tmax 2
tile.minimum buffer.minimum pto.tmin 2
tile.abs buffer.abs pto.tabs 1
tile.exp buffer.exp pto.texp 1
tile.sqrt buffer.sqrt pto.tsqrt 1
tile.neg buffer.neg pto.tneg 1
tile.relu buffer.relu pto.trelu 1
tile.log buffer.log pto.tlog 1
tile.recip buffer.recip pto.trecip 1
tile.adds buffer.adds pto.tadds buffer, scalar
tile.subs buffer.subs pto.tsubs buffer, scalar
tile.muls buffer.muls pto.tmuls buffer, scalar
tile.divs buffer.divs pto.tdivs buffer, scalar
tile.maximums buffer.maximums pto.tmaxs buffer, scalar
tile.minimums buffer.minimums pto.tmins buffer, scalar
tile.lrelu buffer.lrelu pto.tlrelu buffer, scalar
tile.full buffer.full pto.texpands scalar

每个调用以最终操作数表示目标,并返回 Void。源读取数据和元数据;目标写入 有效数据区域并读取元数据。这不保证整个分配已初始化。所有 buffer 操作数必须具有 相同的物理描述符;广播和部分合并语义由独立配方处理。这些算子仅供编译器内部 使用,不新增公开 DSL 函数。

除 add、mul 外的配方要求 FP32、1 或 2 维、静态 valid 范围、 稠密行主序 Vec 存储、none_box、fractal 512,并且没有 padding 或 compact 模式。 其 dtype 约束不会继承原生描述符格式化器的 FP16 支持。add/mul 要求 FP16/FP32/INT32 操作数。自动转换及普通 GM 传输支持静态二维 FP16/BF16/FP32/INT32 描述符;BF16 算术需要独立的原生配方。 Ascend910 与 Ascend950 都使用这些契约。

公开 tile.full 包装器同时接受数值字面量和已解析的标量常量,位置参数与关键字参数 得到一致结果。浮点目标的整数占位常量使用 ConstFloat;显式指定类型的常量保留 原始 dtype,直到降低阶段转换。Python 打印器将 tile.full 填充常量输出为 pl.const(value, dtype),使打印后重新解析仍保留填充值自身的 dtype,而不依赖 tile 的 dtype。运行时填充值仍不支持。

在 Buffer call 边界,标量操作数必须匹配目标元素 dtype,且不具有内存效果。 降低阶段在构造该 call 之前转换受支持的源标量表达式,为有符号整数、 INDEX、FP16、BF16 和 FP32 插入显式 Cast;INDEX 先转换为 INT64。 其他标量源类型仍不支持。tile.full(shape, dtype=..., value=...) 的 shape/dtype 用于选择目标,随后生成 buffer.full(value_f32, destination)。代码生成不修复标量类型:

# Logical input: result = tile.adds(value, count_i32)
scalar_f32 = ir.Cast(count_i32, DataType.FP32, span)
# Buffer IR diagnostic notation; the internal op has no public DSL wrapper.
buffer.adds(value_buffer, scalar_f32, result_buffer)  # -> Void

div、log、recip 保留可选布尔 kwarg high_precision。配方选择相应的原生 精度属性,false 使用原生默认值,不隐式引入 workspace、复制或分配。recip 要求源与目标存储不重叠,构造时拒绝两个位置使用同一句柄。BufferIR 验证对 表中每个配方检查不同句柄的已分配范围:拒绝部分重叠,仅支持精确原地执行的 配方可接受完全相同的窗口。独立的无地址分配不重叠;未知存储来源或位置需要 后续配方。常量标量地址定义通过记忆化及整数位宽检查求值。直接发射消费此已 验证约束,不执行分配或别名分析。

表中配方接受 valid extent 为静态或运行时(-1)的稠密描述符;原生指令读取每个句柄 当前的 valid 元数据,执行时各操作数的元数据必须一致。

整数/位运算、rsqrt 及其可选 workspace、部分合并、广播、归约、 随机生成和其他布局仍属于独立迁移工作。公开 API backend.get_buffer_elementwise_recipe_names() 从实际生产表返回排序的独立快照。 列出的名称仅代表上述受限配方,不代表逻辑算子的全部形式。测试覆盖公开流水线 转换、二进制往返、BufferIR 验证、显式目标身份和原生编译;这些验证不替代设备 数值验收。

类型系统

// 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)

参数访问声明、写通道及核放置/复制契约见算子效应。 Buffer 阶段效应见 Buffer 契约。

类型推导签名:

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);

对于两个普通二维 tensor,tensor.matmul 保留物理输出 shape [lhs M, rhs N],并推导 valid_shape=[lhs valid M, rhs valid N]。两者采用 a_trans/b_trans 指定的相同轴映射。 输入省略有效形状时使用完整 shape;全有效结果仍规范化为无 view 的类型。有效 M/N 可以是 符号表达式,推导会保留表达式。输出拥有新存储,不继承输入的 stride、布局或 padding 元数据。

例如物理 [32,128]、有效 [17,128] 与物理 [128,32]、有效 [128,24] 相乘,得到 物理 [32,32]、有效 [17,24],可以直接 assemble 到 [17,24] 目标,无需 pl.set_validshape。这只是元数据推导,不新增 padding、清零或 K padding 硬件支持。 一维、batched、distributed-window 和 tensor.matmul_acc 的类型推导保持不变。

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

对于 Ascend950 上的二维 FP32 操作数,matmul、matmul_acc 和 tile.matmul_bias 支持仅限关键字参数 tf32_mode="round_even" 或 tf32_mode="round_away"。两者将输入舍入为 HF32(e8m10),仍使用 FP32 累加; 中间值分别采用向偶数舍入或远离零舍入。省略该参数时保持普通 FP32 矩阵乘法。 该设置贯穿 tensor 到 tile 的降级和自动 L0 分块,仅作用于指定操作。 需要 PTOAS v0.70 或更新版本的 PTO/EmitC 路径,不支持 VPTO 降级。 其他输入类型、非二维操作数和 Ascend910 会被拒绝。 Torch 参考代码生成器会在 Tensor、Tile 和 Buffer IR 中按指定模式舍入两个乘法操作数; 偏置与累加器的值保持 FP32。

条件式累加器初始化(init_cond)

tile.matmul_acc、tile.batch_matmul_acc、tensor.matmul_acc 与 tile.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.tmatmul 与 pto.tmatmul.acc 是两个独立算子、且不带 init 操作数。由于 matmul_acc 是原地操作(set_output_reuses_input(0)),两个分 支写入同一缓冲区,scf.if 不产生返回值 —— Acc tile 上不会生成 phi。

字面量谓词一律是 ConstBool,无论是用户写的 init_cond=True/False,还是由 pass 判定得到的 —— 例如 LowerPipelineLoops 复制 K-loop 后生成的 ko == 0。emitter 同样接受 BOOL 类型的 ConstInt,即直接通过 绑定构建 IR 时的写法。两者都会直接选定一个分支;若 emitter 两者都不折叠,每个 K block 都会发出双倍 MAD。

因此该折叠取决于 trip count,并非普遍成立:在 16x512x64 下流水循环被完全消除, 最终 PTO 中没有 scf.if;而在 16x2048x64 下主循环保留,副本索引为 ko 与 ko + 256。Simplify 依据循环的静态 范围判定副本 1 的谓词(ko == -256)恒为假,并将其折叠为 ConstBool;副本 0 的 ko == 0 是真正的运行期测试(它负责初始化累加器),因此恰好保留一个 scf.if。

编译器自己也使用它所推荐的写法: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_matmul 为 TileType 操作数提供批量语义。它接受 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 为 FP8E8M0;lhs_scale 与 rhs_scale 必须是不同的 tile。支持的打包输入形式为任一 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 == 0、K % 64 == 0、N % 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↔Left 或 RightScale↔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.slice、tensor.reshape、tensor.transpose、 tensor.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、步长和偏移均按字节载体计数。 Torch E2M1 x2 张量与显式标注的 UINT8 载体在 JIT 和编译后调用中保留这些维度, 不进行单位换算。

两种打包 FP4 格式到 FP8 的显式转换均在 A5 上经 BF16 和 FP32 合法化。 它仅对数据操作数进行数值转换,因此缩放因子不变。原生打包 FP4 矩阵乘仍不支持。 pl.quant_mx 支持 MXFP8,以及以 FP16/BF16 为源的 E2M1 MXFP4 量化; 只有 FP8 结果可直接供 matmul_mx 使用。量化对齐与内存规划器要求见 FP4。

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 输入先将打包的左操作数转换为 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==0、K%64==0、N%32==0;DeduceTileMatMulMxType 仅对常量维强制;符号维跳过数值检查

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 PTOA5NormalizeTMovPass 把 tget_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_addr;pto.tquant.mx 见 LowerCompositeOps

仅 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_major 和 slayout=none_box,从而选择 PTO-ISA 的行向量提取路径。

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

tile.gemv、tile.gemv_acc 和 tile.gemv_bias 的 acc_phase 可设为 pl.AccPhase.Unspecified(默认值)、pl.AccPhase.Partial 或 pl.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.log 的 high_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_invalid、task_is_valid、available_cluster_count、available_aiv_count) 位置:src/ir/op/sync_ops/ —— sync.cpp(屏障)、task.cpp(TaskId)、launch.cpp(启动形状查询) Python API:from pypto.ir.op import system

公开 Python 屏障接口为 pl.system.barrier(*, pipe),仅接受 pl.PipeType.V、 M、ALL。它构造下表中已有的零操作数 IR 算子,不新增 system.barrier IR 算子或 pipe IR 属性。Python IR printer 根据对应枚举成员,统一打印为 pl.system.barrier(pipe=...) 的写法。原有 bar_v()、bar_m()、 bar_all() Python 接口已移除。屏障不占用事件槽;ALL 为当前核 所有流水线建立顺序,不等待其他核到达。

操作 描述 Kwargs
system.bar_all 当前核所有流水线屏障;Python 接口为 barrier(pipe=pl.PipeType.ALL)(下降为 pto.barrier <PIPE_ALL>) 无
system.bar_v 向量流水线屏障;Python 接口为 barrier(pipe=pl.PipeType.V)(下降为 pto.barrier <PIPE_V>) 无
system.bar_m 矩阵流水线屏障;Python 接口为 barrier(pipe=pl.PipeType.M)(下降为 pto.barrier <PIPE_M>) 无
system.fence 全局内存屏障(下降为 pto.fence.barrier_all #pto.fence_scope<gm>) 无
system.cacheinvalid 使 tensor 子区域基地址所在的那一条 cache line 失效。参数:tensor、shapes(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.set_flag / system.wait_flag 显式核内流水线事件 src_pipe、dst_pipe、静态 event_id 或一个动态 INDEX 操作数;可选 core_type
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] 无

显式 flag 调用的结果为 UnknownType,并作为 EvalStmt(Call) 保留。 src_pipe 与 dst_pipe 必须是不同的具体 PipeType 值(不能是 ALL)。 事件形式必须恰好选择一种:[0, 7] 内的常量 event_id,或一个运行时值也须在 该范围内的 ScalarType(INDEX) 操作数。可选 core_type 是混合核放置所需的 "aic" / "aiv" IR 元数据。代码生成对应 pto.set_flag / pto.wait_flag, 动态 INDEX 操作数则生成 _dyn 形式。动态操作数即使被折叠为常量,仍作为操作数 生成 _dyn 形式;IR 打印通过 pl._dump.call 保留这一表示。这些调用不表示跨核同步。 PipelineLoopValid 验证器会在流水线调度前拒绝 ForKind::Pipeline 循环中的 任一操作。即使关闭可选 IR 验证,调度 pass 也会拒绝这种放置方式;普通循环可以使用显式 flag。

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_type 与 mode 属性在 IR 中仍是字符串,但 Python 接口是枚举:pl.KernelType(AIC / AIV / MIX,表示算子属于展开后的哪个 kernel)与 pl.SyncAllMode(HARD / 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 种子)时,它就是 None 在 with 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 API:import 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.AIV 或 core_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_buf、v2c_consumer_buf,i32 SSA) dir_mask, slot_size,可选 slot_num,可选 local_slot_num,可选 id
system.aiv_initialize_pipe 2 在 Vector 侧初始化跨核管道(位置参数:c2v_consumer_buf、v2c_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)显式指定本地槽数量。
  • 预留/导入缓冲区大小需由用户自行设置,且与架构相关:a3 为 slot_size * local_slot_num;a5 为 slot_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 侧缓冲区实参需为未使用方向的占位(0、pl.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-only。TPREFETCH_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.cpp、matmul.cpp、reduction.cpp,或 src/ir/op/tile_ops/memory.cpp、unary.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.h 和 include/pypto/ir/;注册表与类型推断实现在 src/ir/,算子实现按类别位于 src/ir/op/{tensor_ops,tile_ops,sync_ops}/。

寄存器操作

reg.* P0 schema 使用 RegOp 类别、显式参数效应及寄存器区域要求。 dtype、内存、转换和归属契约见寄存器操作。