Merged
feat(pro): Improve frontend diagnostics and contract checks #6868
ddddweAdd on 29 days ago
feat(pro): Improve frontend diagnostics and contract checks #6868
Merged
ddddweAdd on 29 days ago
Total: 35 files changed+1313-103
@@ -0,0 +1,33 @@
1+# pypto_pro.language.adds
2+ 
3+## 产品支持情况
4+ 
5+- Ascend 950PR&950DT 系列产品:支持。
6+- Atlas A2/A3 系列产品:不支持。
7+ 
8+## 功能说明
9+ 
10+逐元素标量加法,结果为 `out[i, j] = lhs[i, j] + rhs`。这是 [pypto_pro.language.add](add.md) 的 Tile-Scalar 调用形式;允许 out 与 lhs 指向同一 Tile。
11+ 
12+## 函数原型
13+ 
14+```python
15+pypto_pro.language.adds(out: Tile, lhs: Tile, rhs: Scalar) -> None
16+```
17+ 
18+## 参数说明
19+ 
20+| 参数 | 输入/输出 | 说明 |
21+| --- | --- | --- |
22+| out | 输出 | 结果 Tile,位于 Vec(UB)。 |
23+| lhs | 输入 | 输入 Tile,位于 Vec(UB),shape 和 dtype 与 out 相同。 |
24+| rhs | 输入 | 标量操作数;不是 Tile,不参与 Tile-Tile 广播。 |
25+ 
26+## 调用示例
27+ 
28+```python
29+with pl.section_vector():
30+ pl.adds(out_tile, input_tile, 0.5)
31+```
32+ 
33+接口按原生 block.adds 解析,auto_mutex 根据 out/lhs 的写/读角色工作。具体 dtype 支持遵循对应 Tile-Scalar 操作与当前工具链约束;接口声明本身不扩大硬件支持范围。
@@ -6,12 +6,15 @@
6 6 
7abs7abs
8add8add
9+adds
9and_10and_
10div11div
11maximum12maximum
12mul13mul
14+muls
13neg15neg
14relu16relu
15sub17sub
18+subs
16xor19xor
17```20```
@@ -0,0 +1,33 @@
1+# pypto_pro.language.muls
2+ 
3+## 产品支持情况
4+ 
5+- Ascend 950PR&950DT 系列产品:支持。
6+- Atlas A2/A3 系列产品:不支持。
7+ 
8+## 功能说明
9+ 
10+逐元素标量乘法,结果为 `out[i, j] = lhs[i, j] * rhs`。这是 [pypto_pro.language.mul](mul.md) 的 Tile-Scalar 调用形式;允许 out 与 lhs 指向同一 Tile。
11+ 
12+## 函数原型
13+ 
14+```python
15+pypto_pro.language.muls(out: Tile, lhs: Tile, rhs: Scalar) -> None
16+```
17+ 
18+## 参数说明
19+ 
20+| 参数 | 输入/输出 | 说明 |
21+| --- | --- | --- |
22+| out | 输出 | 结果 Tile,位于 Vec(UB)。 |
23+| lhs | 输入 | 输入 Tile,位于 Vec(UB),shape 和 dtype 与 out 相同。 |
24+| rhs | 输入 | 标量操作数;不是 Tile,不参与 Tile-Tile 广播。 |
25+ 
26+## 调用示例
27+ 
28+```python
29+with pl.section_vector():
30+ pl.muls(out_tile, input_tile, 0.5)
31+```
32+ 
33+接口按原生 block.muls 解析,auto_mutex 根据 out/lhs 的写/读角色工作。具体 dtype 支持遵循对应 Tile-Scalar 操作与当前工具链约束;接口声明本身不扩大硬件支持范围。
@@ -0,0 +1,33 @@
1+# pypto_pro.language.subs
2+ 
3+## 产品支持情况
4+ 
5+- Ascend 950PR&950DT 系列产品:支持。
6+- Atlas A2/A3 系列产品:不支持。
7+ 
8+## 功能说明
9+ 
10+逐元素标量减法,结果为 `out[i, j] = lhs[i, j] - rhs`。这是 [pypto_pro.language.sub](sub.md) 的 Tile-Scalar 调用形式;允许 out 与 lhs 指向同一 Tile。
11+ 
12+## 函数原型
13+ 
14+```python
15+pypto_pro.language.subs(out: Tile, lhs: Tile, rhs: Scalar) -> None
16+```
17+ 
18+## 参数说明
19+ 
20+| 参数 | 输入/输出 | 说明 |
21+| --- | --- | --- |
22+| out | 输出 | 结果 Tile,位于 Vec(UB)。 |
23+| lhs | 输入 | 输入 Tile,位于 Vec(UB),shape 和 dtype 与 out 相同。 |
24+| rhs | 输入 | 标量操作数;不是 Tile,不参与 Tile-Tile 广播。 |
25+ 
26+## 调用示例
27+ 
28+```python
29+with pl.section_vector():
30+ pl.subs(out_tile, input_tile, 0.5)
31+```
32+ 
33+接口按原生 block.subs 解析,auto_mutex 根据 out/lhs 的写/读角色工作。具体 dtype 支持遵循对应 Tile-Scalar 操作与当前工具链约束;接口声明本身不扩大硬件支持范围。
@@ -0,0 +1,42 @@
1+# 前端诊断与 IR 观测
2+ 
3+编译期错误中的源码位置指向用户 Kernel。错误包含类型、错误码、原因和可选 hint;源文件可读取时还会显示附近源码与标记。Tile 地址、容量、有效范围和搬运路径检查使用结构化位置,而不是把位置拼接进原因文本。
4+ 
5+直接调用 `make_tile_expr` 或 `_ir_reinterpret` 时,地址、容量、对齐和 buffer 大小的错误及 warning 优先使用显式提供的 `span`;未提供时继承当前 `current_span` 上下文的位置。
6+ 
7+## 编译期 warning
8+ 
9+warning 继承 `pypto_pro._errors.PyptoProWarning`,可使用 Python warning filter 控制。默认启用的 `CapacityUnknown` 表示某个有固定容量的片上 buffer 无法查询容量,检查因此跳过;应核对目标架构、安装的 native binding 和 platform ini。MX scale 的 `ScaleLeft/ScaleRight` 同样有固定片上容量,查询失败时也会报警。DDR 不属于固定片上 buffer,不触发这类 warning。
10+ 
11+`MutexMismatch` 表示手写 mutex ID 的 TileGroup 槽地址重叠,但没有共享 ID,`auto_mutex` 无法通过这些锁为同一段字节建立顺序。该 warning 仅在 `auto_mutex=True` 时检查,复用原生 TileGroup 元数据;AUTO 分配已有的冲突拒绝先执行。不重叠的地址可以复用手写 ID,不因此报警。
12+ 
13+动态偏移是合法写法,因偏移不是编译期常量而跳过的负值和边界检查默认保持安静。需要排查未检查的内容时,设置 `PYPTOPRO_INFO=skipped-checks` 或 `PYPTOPRO_INFO=all`,会产生带 Kernel 位置的 `CheckSkipped` warning;这不表示动态偏移已被证明安全。
14+ 
15+```bash
16+PYPTOPRO_INFO=skipped-checks python kernel.py
17+python -m pytest -W error::pypto_pro._errors.CapacityUnknown
18+```
19+ 
20+Tile 容量检查在构建时执行,第一处超限立即抛出 `OutOfRange` 并终止当前解析。正常编译解析和 pipeline 探测解析均使用这一行为,错误位置指向超限的 Tile 声明。
21+ 
22+TileType 的结构相等性同时检查有效 MemRef 是否存在和 memory space 是否一致。两侧均未指定 MemRef 时可以相等;仅一侧指定时不相等。分配地址和大小不参与类型比较。
23+ 
24+JIT 在编译绑定和 launch 时使用相同的 Tensor dtype 检查。FP4、HF4、INT4 和 UINT4 使用打包的 `torch.uint8` carrier;FP4E2M1 还接受当前 Torch 提供的 `torch.float4_e2m1fn_x2`。FP8E4M3FN、FP8E5M2 和 FP8E8M0 接受各自对应的 Torch dtype 或原始 `torch.uint8` carrier,不混用不同 FP8 格式。INT16 和 UINT16 均接受 `torch.int16` 或 `torch.uint16` carrier;UINT32 接受 `torch.uint32` 或 `torch.int32` carrier。相同位宽的整数 carrier 按原始位模式传入,不进行数值转换。其他 Tensor 参数使用与声明对应的 Torch dtype;检查只验证存储类型,不进行数值转换。4-bit 参数的宿主末维按两个逻辑元素/存储单元换算,显式 `static_shapes` 已是逻辑元素数。
25+ 
26+## 诊断断言
27+ 
28+`str(error)` 保留源码预览。测试中可先用 `pytest.raises(...) as caught` 捕获异常,再对 `caught.value.message` 断言错误原因,避免匹配到源码预览中的文本。需要同时检查错误码、位置、hint 和 note 时,显式检查 `caught.value.render(with_source=False)` 的结果。
29+ 
30+## 只读 IR attribute 快照
31+ 
32+`pypto_pro.ir.get_attributes(node)` 支持 Call、Function 和 ForStmt,返回完整的受支持 attribute 字典,包括列表与表达式元数据。未知 attribute 类型会带字段名抛出 TypeError,不静默丢弃。容器为副本,表达式值仍引用原生 IR 对象;观测时不得修改它们。
33+ 
34+```python
35+from pypto_pro import ir
36+ 
37+if getattr(ir, "attributes_api_version", None) != 1:
38+ raise RuntimeError("This reader requires attributes API version 1")
39+attributes = ir.get_attributes(node)
40+```
41+ 
42+内部 `KernelDef.parse_target_program(..., observer=...)` 提供本次调用局部的 `observer(program, parser)`,在 AUTO mutex ID 分配后、返回最终 Program 前调用。parser 的辅助表保留解析期元数据,不能假定其中的 AUTO ID placeholder 已随 Program 一起替换。observer 异常传给调用方,不保留为后续调用的回调;该内部入口需要针对实际安装版本进行能力探针。
@@ -7,6 +7,7 @@
7:titlesonly:7:titlesonly:
8 8 
9functional_debugging9functional_debugging
10+frontend_diagnostics
10sanitizer11sanitizer
11performance_optimization12performance_optimization
12```13```
@@ -725,6 +725,18 @@ bool StructuralEqualImpl<AssertMode>::EqualType(const TypePtr& lhs, const TypePt
725 if (!Equal(lhs_tile->shape_[i], rhs_tile->shape_[i]))725 if (!Equal(lhs_tile->shape_[i], rhs_tile->shape_[i]))
726 return false;726 return false;
727 }727 }
728+ // Buffer kind is part of a Tile's C++ type; address and size belong to the allocation.
729+ const auto& lhs_memref = lhs_tile->memref_;
730+ const auto& rhs_memref = rhs_tile->memref_;
731+ const bool lhs_has_memref = lhs_memref && *lhs_memref;
732+ const bool rhs_has_memref = rhs_memref && *rhs_memref;
733+ if (lhs_has_memref != rhs_has_memref) {
734+ return IRAssert({}, "TileType memory reference presence mismatch");
735+ }
736+ if (lhs_has_memref && (*lhs_memref)->memorySpace_ != (*rhs_memref)->memorySpace_) {
737+ return IRAssert({}, "TileType memory space mismatch (", MemorySpaceToString((*lhs_memref)->memorySpace_),
738+ " != ", MemorySpaceToString((*rhs_memref)->memorySpace_), ")");
739+ }
728 // Compare tile_view740 // Compare tile_view
729 if (lhs_tile->tileView_.has_value() != rhs_tile->tileView_.has_value()) {741 if (lhs_tile->tileView_.has_value() != rhs_tile->tileView_.has_value()) {
730 return IRAssert({}, "TileType tile_view presence mismatch");742 return IRAssert({}, "TileType tile_view presence mismatch");
@@ -232,6 +232,41 @@ TEST_F(IRStructEqTypeTest, TestTensorAndTileTypeEqual)
232 EXPECT_FALSE(structural_equal(compactNullTile, compactNormalTile));232 EXPECT_FALSE(structural_equal(compactNullTile, compactNormalTile));
233}233}
234 234 
235+TEST_F(IRStructEqTypeTest, TestTileTypeMemoryReferencePresence)
236+{
237+ auto dim = std::make_shared<ConstInt>(16, DataType::INT64, Sp());
238+ auto offset = std::make_shared<ConstInt>(0, DataType::INDEX, Sp());
239+ MemRefPtr memref = std::make_shared<MemRef>(MemorySpace::Mat, offset, 1024, Sp());
240+ auto known = std::make_shared<TileType>(std::vector<ExprPtr>{dim}, DataType::FP32, memref);
241+ auto absent = std::make_shared<TileType>(std::vector<ExprPtr>{dim}, DataType::FP32);
242+ auto null = std::make_shared<TileType>(std::vector<ExprPtr>{dim}, DataType::FP32,
243+ std::optional<MemRefPtr>(MemRefPtr{}));
244+ for (const auto& missing : {absent, null}) {
245+ EXPECT_FALSE(structural_equal(known, missing));
246+ EXPECT_FALSE(structural_equal(missing, known));
247+ EXPECT_THROW(assert_structural_equal(known, missing), npu::tile_fwk::Error);
248+ }
249+ EXPECT_TRUE(structural_equal(absent, null));
250+ EXPECT_TRUE(structural_equal(null, absent));
251+ EXPECT_TRUE(structural_equal(null, null));
252+}
253+ 
254+TEST_F(IRStructEqTypeTest, TestTileTypeMemorySpaceIgnoresAllocation)
255+{
256+ auto dim = std::make_shared<ConstInt>(16, DataType::INT64, Sp());
257+ auto offset = std::make_shared<ConstInt>(0, DataType::INDEX, Sp());
258+ auto otherOffset = std::make_shared<ConstInt>(4096, DataType::INDEX, Sp());
259+ MemRefPtr mat = std::make_shared<MemRef>(MemorySpace::Mat, offset, 1024, Sp());
260+ MemRefPtr otherMat = std::make_shared<MemRef>(MemorySpace::Mat, otherOffset, 2048, Sp());
261+ MemRefPtr vec = std::make_shared<MemRef>(MemorySpace::Vec, offset, 1024, Sp());
262+ auto lhs = std::make_shared<TileType>(std::vector<ExprPtr>{dim}, DataType::FP32, mat);
263+ auto sameSpace = std::make_shared<TileType>(std::vector<ExprPtr>{dim}, DataType::FP32, otherMat);
264+ auto otherSpace = std::make_shared<TileType>(std::vector<ExprPtr>{dim}, DataType::FP32, vec);
265+ EXPECT_TRUE(structural_equal(lhs, sameSpace));
266+ EXPECT_FALSE(structural_equal(lhs, otherSpace));
267+ EXPECT_FALSE(structural_equal(otherSpace, lhs));
268+}
269+ 
235TEST_F(IRStructEqTypeTest, TestTupleAndOtherTypes)270TEST_F(IRStructEqTypeTest, TestTupleAndOtherTypes)
236{271{
237 auto s32 = std::make_shared<ScalarType>(DataType::INT32);272 auto s32 = std::make_shared<ScalarType>(DataType::INT32);
@@ -28,6 +28,11 @@ from __future__ import annotations
28 28 
29__all__ = [29__all__ = [
30 "PyptoProError",30 "PyptoProError",
31+ "PyptoProWarning",
32+ "CapacityUnknown",
33+ "CheckSkipped",
34+ "MutexMismatch",
35+ "warn_check_skipped",
31 "argument_span",36 "argument_span",
32 "remembered_span",37 "remembered_span",
33 "source_lines_of",38 "source_lines_of",
@@ -62,6 +67,7 @@ import contextvars
62import os67import os
63import re68import re
64import sys69import sys
70+import warnings
65 71 
66from pypto.error import PyptoError72from pypto.error import PyptoError
67from pypto.pypto_impl import ir73from pypto.pypto_impl import ir
@@ -377,6 +383,44 @@ _PRIOR_CONTEXT_LINES = 2
377_FOLLOWING_CONTEXT_LINES = 2383_FOLLOWING_CONTEXT_LINES = 2
378 384 
379 385 
386+class PyptoProWarning(UserWarning):
387+ """A located compile-time warning, filterable with Python's warning categories."""
388+ 
389+ 
390+class CapacityUnknown(PyptoProWarning):
391+ """An on-chip buffer capacity could not be queried, so its bounds were not checked."""
392+ 
393+ 
394+class CheckSkipped(PyptoProWarning):
395+ """An offset check was skipped; opt in with PYPTOPRO_INFO=skipped-checks or all."""
396+ 
397+ 
398+class MutexMismatch(PyptoProWarning):
399+ """Overlapping manually configured Tile buffers have no shared mutex ID."""
400+ 
401+ 
402+_WARNING_REGISTRIES: dict[str, dict] = {}
403+ 
404+ 
405+def _warn_located(message: str, category: type[Warning], span=None) -> None:
406+ """Use the DSL span instead of a stacklevel through the parser's variable-depth calls."""
407+ location = _normalize_span(span if span is not None else _CURRENT_SPAN.get()) or {}
408+ filename = location.get("filename")
409+ line = location.get("line") or location.get("begin_line")
410+ if filename and line:
411+ registry = _WARNING_REGISTRIES.setdefault(str(filename), {})
412+ warnings.warn_explicit(message, category, str(filename), int(line), registry=registry)
413+ else:
414+ warnings.warn(message, category, stacklevel=3)
415+ 
416+ 
417+def warn_check_skipped(message: str, span: ir.Span | None = None) -> None:
418+ """Report skipped checks only when requested, without changing the validation result."""
419+ categories = {part.strip() for part in os.environ.get("PYPTOPRO_INFO", "").split(",")}
420+ if categories.intersection({"skipped-checks", "all"}):
421+ _warn_located(message, CheckSkipped, span)
422+ 
423+ 
380class PyptoProError(PyptoError):424class PyptoProError(PyptoError):
381 """Base class for every pypto_pro error.425 """Base class for every pypto_pro error.
382 426 
@@ -442,15 +486,19 @@ class PyptoProError(PyptoError):
442 self.previous_span = _normalize_span(previous_span)486 self.previous_span = _normalize_span(previous_span)
443 487 
444 def __str__(self) -> str:488 def __str__(self) -> str:
489+ return self.render()
490+ 
491+ def render(self, *, with_source: bool = True) -> str:
492+ """Render the diagnostic and location, optionally omitting the quoted source."""
445 parts = [super().__str__()]493 parts = [super().__str__()]
446- self._append_location(parts)494+ self._append_location(parts, with_source=with_source)
447 if self.hint:495 if self.hint:
448 parts.append(f" hint: {self.hint}")496 parts.append(f" hint: {self.hint}")
449 if self.note:497 if self.note:
450 parts.append(f" note: {self.note}")498 parts.append(f" note: {self.note}")
451 return "\n".join(parts)499 return "\n".join(parts)
452 500 
453- def _append_location(self, parts: list) -> None:501+ def _append_location(self, parts: list, *, with_source: bool = True) -> None:
454 if not self.span:502 if not self.span:
455 return503 return
456 filename = self.span.get("filename") or "<unknown>"504 filename = self.span.get("filename") or "<unknown>"
@@ -459,6 +507,8 @@ class PyptoProError(PyptoError):
459 return507 return
460 column = self.span.get("column") or self.span.get("begin_column")508 column = self.span.get("column") or self.span.get("begin_column")
461 parts.append(f" --> {filename}:{line}:{column}")509 parts.append(f" --> {filename}:{line}:{column}")
510+ if not with_source:
511+ return
462 if self.source_lines is None and self.span.get("filename"):512 if self.source_lines is None and self.span.get("filename"):
463 # Not every entry point attaches the source (a pipeline probe parse513 # Not every entry point attaches the source (a pipeline probe parse
464 # calls the parser directly), so read the file the span names.514 # calls the parser directly), so read the file the span names.
@@ -24,6 +24,8 @@ __all__ = [
24 "TensorType",24 "TensorType",
25 "TileType",25 "TileType",
26 "python_print",26 "python_print",
27+ "get_attributes",
28+ "attributes_api_version",
27 "VerificationMode",29 "VerificationMode",
28 "VerificationLevel",30 "VerificationLevel",
29 "PassContext",31 "PassContext",
@@ -46,6 +46,7 @@ from pypto.pypto_impl.ir import TileType as _IRTileType # IR-level TileType (C+
46from pypto_pro.ir._utils import _is_int, _normalize_expr, _to_make_tuple46from pypto_pro.ir._utils import _is_int, _normalize_expr, _to_make_tuple
47 47 
48from ..._errors import (48from ..._errors import (
49+ CapacityUnknown,
49 DynamicShapeUnsupported,50 DynamicShapeUnsupported,
50 InvalidArgument,51 InvalidArgument,
51 InvalidFormat,52 InvalidFormat,
@@ -57,7 +58,9 @@ from ..._errors import (
57 NotSupported,58 NotSupported,
58 OutOfRange,59 OutOfRange,
59 PyptoProError,60 PyptoProError,
61+ _warn_located,
60 message_of,62 message_of,
63+ warn_check_skipped,
61)64)
62 65 
63# NOTE: a DSL-descriptor dataclass named ``TileType`` is defined later in this66# NOTE: a DSL-descriptor dataclass named ``TileType`` is defined later in this
@@ -1093,7 +1096,6 @@ def _ir_col_sum(out: Expr, src: Expr, tmp: Expr, *, span: Span | None = None) ->
1093def _validate_validshape_bounds(1096def _validate_validshape_bounds(
1094 tile: Expr,1097 tile: Expr,
1095 valid_shape: Sequence[int | Expr] | _ir_core.MakeTuple,1098 valid_shape: Sequence[int | Expr] | _ir_core.MakeTuple,
1096- span: Span | None = None,
1097) -> None:1099) -> None:
1098 """Validate that a Tile supports runtime valid-shape updates and that dimensions are in bounds.1100 """Validate that a Tile supports runtime valid-shape updates and that dimensions are in bounds.
1099 1101 
@@ -1124,7 +1126,6 @@ def _validate_validshape_bounds(
1124 tile_shape = _tile_shape_ints(tile_type)1126 tile_shape = _tile_shape_ints(tile_type)
1125 1127 
1126 dim_names = ("row", "col")1128 dim_names = ("row", "col")
1127- span_info = f" at {span}" if span else ""
1128 1129 
1129 for i, vs in enumerate(valid_shape):1130 for i, vs in enumerate(valid_shape):
1130 dim_name = dim_names[i]1131 dim_name = dim_names[i]
@@ -1134,7 +1135,7 @@ def _validate_validshape_bounds(
1134 if vs <= 0:1135 if vs <= 0:
1135 raise InvalidShape(1136 raise InvalidShape(
1136 f"set_validshape {dim_name}={vs} must be positive "1137 f"set_validshape {dim_name}={vs} must be positive "
1137- f"(got {vs}){span_info}. "1138+ f"(got {vs}). "
1138 f"Valid shape dimensions must be >= 1."1139 f"Valid shape dimensions must be >= 1."
1139 )1140 )
1140 1141 
@@ -1143,9 +1144,8 @@ def _validate_validshape_bounds(
1143 ts_val = tile_shape[i]1144 ts_val = tile_shape[i]
1144 if vs > ts_val:1145 if vs > ts_val:
1145 raise OutOfRange(1146 raise OutOfRange(
1146- f"set_validshape {dim_name}={vs} exceeds tile {dim_name}={ts_val}"1147+ f"set_validshape {dim_name}={vs} exceeds tile {dim_name}={ts_val}. "
1147- f"{span_info}. "1148+ "Valid shape must be <= tile shape."
1148- f"Valid shape must be <= tile shape."
1149 )1149 )
1150 1150 
1151 1151 
@@ -1156,7 +1156,7 @@ def _ir_set_validshape(
1156 span: Span | None = None,1156 span: Span | None = None,
1157) -> Expr:1157) -> Expr:
1158 actual_span = span or _span()1158 actual_span = span or _span()
1159- _validate_validshape_bounds(tile, shape, actual_span)1159+ _validate_validshape_bounds(tile, shape)
1160 shape_0, shape_1 = _normalize_2d_sequence(shape, "shape", actual_span)1160 shape_0, shape_1 = _normalize_2d_sequence(shape, "shape", actual_span)
1161 return _ir_core.create_op_call(block_ir_op("set_validshape"), [tile, shape_0, shape_1], {}, actual_span)1161 return _ir_core.create_op_call(block_ir_op("set_validshape"), [tile, shape_0, shape_1], {}, actual_span)
1162 1162 
@@ -1582,6 +1582,7 @@ def _validate_offsets(
1582 if off_val is not None and off_val < 0:1582 if off_val is not None and off_val < 0:
1583 raise NotSupported(f"{op_name}: offsets[{i}] is {off_val}, negative offset is not allowed")1583 raise NotSupported(f"{op_name}: offsets[{i}] is {off_val}, negative offset is not allowed")
1584 if off_val is None:1584 if off_val is None:
1585+ warn_check_skipped(f"{op_name}: offsets[{i}] is dynamic; negative-offset and bounds checks were skipped")
1585 continue1586 continue
1586 if access_size is not None:1587 if access_size is not None:
1587 t_size = access_size1588 t_size = access_size
@@ -3548,15 +3549,15 @@ def _validate_tile_addr_alignment(
3548 return # DDR / Scaling / Bias — no enforced alignment3549 return # DDR / Scaling / Bias — no enforced alignment
3549 if addr % required != 0:3550 if addr % required != 0:
3550 mem_name = str(target_memory).replace("MemorySpace.", "")3551 mem_name = str(target_memory).replace("MemorySpace.", "")
3551- span_info = f" at {span}" if span else ""
3552 raise InvalidTile(3552 raise InvalidTile(
3553 f"Tile address 0x{addr:05X} ({addr}) is not {required}-byte aligned "3553 f"Tile address 0x{addr:05X} ({addr}) is not {required}-byte aligned "
3554- f"for memory space {mem_name}{span_info}. "3554+ f"for memory space {mem_name}. "
3555- f"Address must be a multiple of {required}."3555+ f"Address must be a multiple of {required}.",
3556+ span=span,
3556 )3557 )
3557 3558 
3558 3559 
3559-def _get_memory_capacity(target_memory: MemorySpace) -> int:3560+def _get_memory_capacity(target_memory: MemorySpace, span: Span | None = None) -> int:
3560 """On-chip buffer capacity (bytes) for ``target_memory``.3561 """On-chip buffer capacity (bytes) for ``target_memory``.
3561 3562 
3562 The target platform comes from the pypto_pro compile flow: ``pl.jit``3563 The target platform comes from the pypto_pro compile flow: ``pl.jit``
@@ -3573,14 +3574,16 @@ def _get_memory_capacity(target_memory: MemorySpace) -> int:
3573 3574 
3574 arch = get_current_arch()3575 arch = get_current_arch()
3575 cap = get_memory_limit(arch, target_memory.name)3576 cap = get_memory_limit(arch, target_memory.name)
3576- if cap <= 0:3577+ if cap <= 0 and target_memory in (
3577- import logging3578+ MemorySpace.Vec, MemorySpace.Mat, MemorySpace.Left, MemorySpace.Right,
3578- 3579+ MemorySpace.Acc, MemorySpace.Bias, MemorySpace.Scaling,
3579- logging.getLogger(__name__).warning(3580+ MemorySpace.ScaleLeft, MemorySpace.ScaleRight,
3580- "Cannot determine %s capacity for arch %s; skipping tile capacity validation. "3581+ ):
3581- "Check that pypto_impl is loaded and the arch maps to an installed platform ini.",3582+ _warn_located(
3582- target_memory,3583+ f"Cannot determine {target_memory.name} capacity for arch {arch}; tile capacity validation "
3583- arch,3584+ "was skipped. Check the installed platform ini and native bindings.",
3585+ CapacityUnknown,
3586+ span,
3584 )3587 )
3585 # The platform ini reports the software-usable UB size, which may be smaller3588 # The platform ini reports the software-usable UB size, which may be smaller
3586 # than the physical buffer (e.g., ub_size=253952 = 256K - 8K reserved). Tile3589 # than the physical buffer (e.g., ub_size=253952 = 256K - 8K reserved). Tile
@@ -3606,25 +3609,24 @@ def _validate_tile_addr_capacity(
3606 compilation and triggers an AICore hardware trap (error 507015) at runtime.3609 compilation and triggers an AICore hardware trap (error 507015) at runtime.
3607 """3610 """
3608 mem_name = str(target_memory).replace("MemorySpace.", "")3611 mem_name = str(target_memory).replace("MemorySpace.", "")
3609- span_info = f" at {span}" if span else ""
3610 if addr < 0:3612 if addr < 0:
3611 raise InvalidTile(3613 raise InvalidTile(
3612- f"Tile addr {addr} is negative for memory space {mem_name}{span_info}. "3614+ f"Tile addr {addr} is negative for memory space {mem_name}. "
3613- f"Address must be a non-negative integer."3615+ f"Address must be a non-negative integer.", span=span,
3614 )3616 )
3615 if size <= 0:3617 if size <= 0:
3616 raise InvalidTile(3618 raise InvalidTile(
3617- f"Tile size {size} must be positive for memory space {mem_name}{span_info}."3619+ f"Tile size {size} must be positive for memory space {mem_name}.", span=span,
3618 )3620 )
3619- capacity = _get_memory_capacity(target_memory)3621+ capacity = _get_memory_capacity(target_memory, span)
3620 if capacity <= 0:3622 if capacity <= 0:
3621 return3623 return
3622 end_addr = addr + size3624 end_addr = addr + size
3623 if end_addr > capacity:3625 if end_addr > capacity:
3624 raise OutOfRange(3626 raise OutOfRange(
3625 f"Tile addr 0x{addr:X} + size {size} (0x{end_addr:X}) exceeds "3627 f"Tile addr 0x{addr:X} + size {size} (0x{end_addr:X}) exceeds "
3626- f"{mem_name} capacity 0x{capacity:X} ({capacity} bytes){span_info}. "3628+ f"{mem_name} capacity 0x{capacity:X} ({capacity} bytes). "
3627- f"Use a smaller address so that addr + tile_size <= {capacity}."3629+ f"Use a smaller address so that addr + tile_size <= {capacity}.", span=span,
3628 )3630 )
3629 3631 
3630 3632 
@@ -3844,7 +3846,7 @@ def _validate_tile_type_params(tt: "TileType") -> None:
3844 raise InvalidShape(3846 raise InvalidShape(
3845 f"TileType Bias tiles must have exactly 1 row (hardware Rows==1 constraint, "3847 f"TileType Bias tiles must have exactly 1 row (hardware Rows==1 constraint, "
3846 f"see TMatmul.hpp/TMov.hpp), got shape {tt.shape!r} with {shape[0]} rows. "3848 f"see TMatmul.hpp/TMov.hpp), got shape {tt.shape!r} with {shape[0]} rows. "
3847- f"Use shape=[1, N] for the Bias (L0B) tile."3849+ f"Use shape=[1, N] for the Bias tile."
3848 )3850 )
3849 3851 
3850 valid_shape = _const_shape_ints(tt.valid_shape)3852 valid_shape = _const_shape_ints(tt.valid_shape)
@@ -3928,19 +3930,20 @@ def make_tile_expr(
3928 }3930 }
3929 kwargs = {k: v for k, v in kwargs.items() if v is not None}3931 kwargs = {k: v for k, v in kwargs.items() if v is not None}
3930 if addr is None:3932 if addr is None:
3931- raise InvalidVal(f"make_tile_expr() requires 'addr', but it is missing. {_MAKE_TILE_HINT}")3933+ raise InvalidVal(f"make_tile_expr() requires 'addr', but it is missing. {_MAKE_TILE_HINT}", span=span)
3932 if size is None:3934 if size is None:
3933 try:3935 try:
3934 size = tile_slot_size(shape_tuple, dtype)3936 size = tile_slot_size(shape_tuple, dtype)
3935 except PyptoProError as exc:3937 except PyptoProError as exc:
3936 raise InvalidOperation(3938 raise InvalidOperation(
3937 f"pl.make_tile() cannot derive its byte span from the tile type: {message_of(exc)}. "3939 f"pl.make_tile() cannot derive its byte span from the tile type: {message_of(exc)}. "
3938- "Use a static positive shape in pl.TileType."3940+ "Use a static positive shape in pl.TileType.",
3941+ span=span,
3939 ) from exc3942 ) from exc
3940 if isinstance(addr, int):3943 if isinstance(addr, int):
3941- _validate_tile_addr_alignment(addr, target_memory, actual_span)3944+ _validate_tile_addr_alignment(addr, target_memory, span)
3942 if isinstance(size, int):3945 if isinstance(size, int):
3943- _validate_tile_addr_capacity(addr, size, target_memory, actual_span)3946+ _validate_tile_addr_capacity(addr, size, target_memory, span)
3944 global mem_id3947 global mem_id
3945 mem_id += 13948 mem_id += 1
3946 kwargs["memref_addr"] = addr3949 kwargs["memref_addr"] = addr
@@ -3998,12 +4001,11 @@ def _parse_make_tile(self, call: ast.Call) -> Expr:
3998 if len(call.args) > 1:4001 if len(call.args) > 1:
3999 raise InvalidArgument(4002 raise InvalidArgument(
4000 f"pl.make_tile() takes 1 positional argument (the tile type) but {len(call.args)} were given",4003 f"pl.make_tile() takes 1 positional argument (the tile type) but {len(call.args)} were given",
4001- span=span,
4002 hint="pass addr as a keyword, e.g. pl.make_tile(tile_type, addr=0x0)",4004 hint="pass addr as a keyword, e.g. pl.make_tile(tile_type, addr=0x0)",
4003 )4005 )
4004 4006 
4005 if not call.keywords:4007 if not call.keywords:
4006- raise InvalidVal("pl.make_tile() missing required keyword 'addr'", span=span, hint=_MAKE_TILE_HINT)4008+ raise InvalidVal("pl.make_tile() missing required keyword 'addr'", hint=_MAKE_TILE_HINT)
4007 if len(call.keywords) != 1 or call.keywords[0].arg != "addr":4009 if len(call.keywords) != 1 or call.keywords[0].arg != "addr":
4008 unsupported = [kw.arg or "**kwargs" for kw in call.keywords if kw.arg != "addr"]4010 unsupported = [kw.arg or "**kwargs" for kw in call.keywords if kw.arg != "addr"]
4009 raise InvalidArgument(4011 raise InvalidArgument(
@@ -4033,10 +4035,6 @@ def _parse_make_tile(self, call: ast.Call) -> Expr:
4033# ---------------------------------------------------------------------------4035# ---------------------------------------------------------------------------
4034 4036 
4035 4037 
4036-def _span_msg(span: "Span | None") -> str:
4037- return f" at {span}" if span else ""
4038- 
4039- 
4040def _hw_attr(hw, field: str) -> "int | None":4038def _hw_attr(hw, field: str) -> "int | None":
4041 """One hardware_info field as an int, or None when unset/negative sentinel."""4039 """One hardware_info field as an int, or None when unset/negative sentinel."""
4042 if hw is None:4040 if hw is None:
@@ -4057,7 +4055,8 @@ def _reinterpret_footprint_check(shape: list[int], dtype: DataType, size: int, s
4057 if footprint > size:4055 if footprint > size:
4058 raise OutOfRange(4056 raise OutOfRange(
4059 f"reinterpret: new footprint {footprint} bytes exceeds the original buffer size {size} bytes; "4057 f"reinterpret: new footprint {footprint} bytes exceeds the original buffer size {size} bytes; "
4060- f"a reinterpretation may only reuse the buffer at the same size or smaller{_span_msg(span)}"4058+ "a reinterpretation may only reuse the buffer at the same size or smaller",
4059+ span=span,
4061 )4060 )
4062 4061 
4063 4062 
@@ -4066,8 +4065,8 @@ def _reinterpret_align_check(addr: int, dtype: DataType, span: "Span | None") ->
4066 elem_bytes = max(1, (int(dtype.get_bit()) + 7) // 8)4065 elem_bytes = max(1, (int(dtype.get_bit()) + 7) // 8)
4067 if addr % elem_bytes != 0:4066 if addr % elem_bytes != 0:
4068 raise InvalidTile(4067 raise InvalidTile(
4069- f"reinterpret: address 0x{addr:X} is not aligned to the new element size {elem_bytes} bytes"4068+ f"reinterpret: address 0x{addr:X} is not aligned to the new element size {elem_bytes} bytes",
4070- f"{_span_msg(span)}"4069+ span=span,
4071 )4070 )
4072 4071 
4073 4072 
@@ -4100,12 +4099,13 @@ def _ir_reinterpret(
4100 """4099 """
4101 actual_span = span or _span()4100 actual_span = span or _span()
4102 if not isinstance(src.type, _IRTileType):4101 if not isinstance(src.type, _IRTileType):
4103- raise InvalidType(f"reinterpret: expected a Tile, got {type(src.type).__name__}")4102+ raise InvalidType(f"reinterpret: expected a Tile, got {type(src.type).__name__}", span=span)
4104 t = src.type4103 t = src.type
4105 memref = getattr(t, "memref", None)4104 memref = getattr(t, "memref", None)
4106 if memref is None:4105 if memref is None:
4107 raise InvalidArgument(4106 raise InvalidArgument(
4108- f"reinterpret: source tile has no MemRef (bind it with pl.make_tile first){_span_msg(actual_span)}"4107+ "reinterpret: source tile has no MemRef (bind it with pl.make_tile first)",
4108+ span=span,
4109 )4109 )
4110 4110 
4111 old_shape = _static_shape_ints(t.shape, "reinterpret: source")4111 old_shape = _static_shape_ints(t.shape, "reinterpret: source")
@@ -4118,9 +4118,9 @@ def _ir_reinterpret(
4118 4118 
4119 addr = _memref_const_addr(memref, actual_span)4119 addr = _memref_const_addr(memref, actual_span)
4120 if addr is None:4120 if addr is None:
4121- raise InvalidType(f"reinterpret: source tile address must be a compile-time constant{_span_msg(actual_span)}")4121+ raise InvalidType("reinterpret: source tile address must be a compile-time constant", span=span)
4122- _reinterpret_align_check(addr, new_dtype, actual_span)4122+ _reinterpret_align_check(addr, new_dtype, span)
4123- _reinterpret_footprint_check(new_shape, new_dtype, int(memref.size), actual_span)4123+ _reinterpret_footprint_check(new_shape, new_dtype, int(memref.size), span)
4124 4124 
4125 # Fresh MemRef at the same address & size (new memref_id); valid_shape4125 # Fresh MemRef at the same address & size (new memref_id); valid_shape
4126 # intentionally omitted so the new handle starts dynamic (-1).4126 # intentionally omitted so the new handle starts dynamic (-1).
@@ -4135,7 +4135,7 @@ def _ir_reinterpret(
4135 fractal=_hw_attr(hw, "fractal"),4135 fractal=_hw_attr(hw, "fractal"),
4136 pad=_hw_attr(hw, "pad"),4136 pad=_hw_attr(hw, "pad"),
4137 compact=_hw_attr(hw, "compact"),4137 compact=_hw_attr(hw, "compact"),
4138- span=actual_span,4138+ span=span,
4139 )4139 )
4140 4140 
4141 4141 
@@ -4146,7 +4146,6 @@ def _parse_reinterpret(self, call: ast.Call) -> Expr:
4146 if len(call.args) != 1:4146 if len(call.args) != 1:
4147 raise InvalidArgument(4147 raise InvalidArgument(
4148 f"pl.reinterpret() takes exactly 1 positional argument (a Tile or TileGroup), got {len(call.args)}",4148 f"pl.reinterpret() takes exactly 1 positional argument (a Tile or TileGroup), got {len(call.args)}",
4149- span=span,
4150 hint="Usage: t2 = pl.reinterpret(tile, dtype=..., shape=..., layout=...)",4149 hint="Usage: t2 = pl.reinterpret(tile, dtype=..., shape=..., layout=...)",
4151 )4150 )
4152 src = self.parse_expression(call.args[0])4151 src = self.parse_expression(call.args[0])
@@ -4156,7 +4155,6 @@ def _parse_reinterpret(self, call: ast.Call) -> Expr:
4156 if kwargs.get("dtype") is None and kwargs.get("shape") is None and kwargs.get("layout") is None:4155 if kwargs.get("dtype") is None and kwargs.get("shape") is None and kwargs.get("layout") is None:
4157 raise InvalidType(4156 raise InvalidType(
4158 "pl.reinterpret() requires at least one of dtype/shape/layout to override",4157 "pl.reinterpret() requires at least one of dtype/shape/layout to override",
4159- span=span,
4160 hint="e.g. pl.reinterpret(tile, shape=[64, 64], dtype=pl.DT_BF16)",4158 hint="e.g. pl.reinterpret(tile, shape=[64, 64], dtype=pl.DT_BF16)",
4161 )4159 )
4162 4160 
@@ -4183,6 +4181,15 @@ def _parse_reinterpret(self, call: ast.Call) -> Expr:
4183 if self.is_tile_group(src):4181 if self.is_tile_group(src):
4184 return self._build_reinterpreted_group(src, kwargs, span)4182 return self._build_reinterpreted_group(src, kwargs, span)
4185 4183 
4184+ if src in self._dynamic_slot_tiles or (
4185+ isinstance(src, _ir_core.GetItemExpr) and not isinstance(src.slice, ConstInt)
4186+ ):
4187+ raise NotSupported(
4188+ "pl.reinterpret() cannot resolve the address of a Tile selected by a runtime slot index; "
4189+ "its static type would bind the view to slot 0",
4190+ hint="Reinterpret the whole TileGroup, then select the matching slot from that view",
4191+ )
4192+ 
4186 new_tile = _ir_reinterpret(src, shape=new_shape, dtype=kwargs.get("dtype"), layout=kwargs.get("layout"), span=span)4193 new_tile = _ir_reinterpret(src, shape=new_shape, dtype=kwargs.get("dtype"), layout=kwargs.get("layout"), span=span)
4187 4194 
4188 # The reinterpreted tile aliases the same buffer, so it inherits the source's4195 # The reinterpreted tile aliases the same buffer, so it inherits the source's
@@ -5027,7 +5034,9 @@ def _parse_matmul_mx(self, call: ast.Call) -> Expr:
5027 args = [self.parse_expression(arg) for arg in call.args]5034 args = [self.parse_expression(arg) for arg in call.args]
5028 kwargs = self.parse_op_kwargs(call)5035 kwargs = self.parse_op_kwargs(call)
5029 self._check_declared_types("matmul_mx", call, args, kwargs, span)5036 self._check_declared_types("matmul_mx", call, args, kwargs, span)
5030- return _ir_matmul_mx(*args, **kwargs, span=span)5037+ result = _ir_matmul_mx(*args, **kwargs, span=span)
5038+ self._check_mx_plane_slots("matmul_mx", args, kwargs, span)
5039+ return result
5031 5040 
5032 5041 
5033@op_impl("matmul_mx_acc")5042@op_impl("matmul_mx_acc")
@@ -5036,7 +5045,9 @@ def _parse_matmul_mx_acc(self, call: ast.Call) -> Expr:
5036 args = [self.parse_expression(arg) for arg in call.args]5045 args = [self.parse_expression(arg) for arg in call.args]
5037 kwargs = self.parse_op_kwargs(call)5046 kwargs = self.parse_op_kwargs(call)
5038 self._check_declared_types("matmul_mx_acc", call, args, kwargs, span)5047 self._check_declared_types("matmul_mx_acc", call, args, kwargs, span)
5039- return _ir_matmul_mx_acc(*args, **kwargs, span=span)5048+ result = _ir_matmul_mx_acc(*args, **kwargs, span=span)
5049+ self._check_mx_plane_slots("matmul_mx_acc", args, kwargs, span)
5050+ return result
5040 5051 
5041 5052 
5042def _to_const_int(val: Any, span: Span | None = None) -> Expr:5053def _to_const_int(val: Any, span: Span | None = None) -> Expr:
@@ -89,8 +89,11 @@ __all__ = [
89 "ssbuf_load",89 "ssbuf_load",
90 # Binary element-wise (unified: tile-tile + tile-scalar)90 # Binary element-wise (unified: tile-tile + tile-scalar)
91 "add",91 "add",
92+ "adds",
92 "sub",93 "sub",
94+ "subs",
93 "mul",95 "mul",
96+ "muls",
94 "div",97 "div",
95 "maximum",98 "maximum",
96 "minimum",99 "minimum",
@@ -284,6 +287,7 @@ from ._api import (
284 add,287 add,
285 add_relu,288 add_relu,
286 add_relu_cast,289 add_relu_cast,
290+ adds,
287 and_,291 and_,
288 argmax,292 argmax,
289 argmin,293 argmin,
@@ -345,6 +349,7 @@ from ._api import (
345 mul,349 mul,
346 mul_add_dst,350 mul_add_dst,
347 mul_cast,351 mul_cast,
352+ muls,
348 ne,353 ne,
349 neg,354 neg,
350 not_,355 not_,
@@ -387,6 +392,7 @@ from ._api import (
387 sub,392 sub,
388 sub_relu,393 sub_relu,
389 sub_relu_cast,394 sub_relu_cast,
395+ subs,
390 sum,396 sum,
391 transpose,397 transpose,
392 trap,398 trap,
@@ -430,14 +436,10 @@ DT_BOOL = DataType.BOOL
430def struct(*args: Any, **kwargs: Any) -> Any:436def struct(*args: Any, **kwargs: Any) -> Any:
431 """Create a compile-time struct grouping IR expressions for attribute access.437 """Create a compile-time struct grouping IR expressions for attribute access.
432 438 
433- Two forms are accepted:439+ Inside a kernel, ``pl.struct("StructName", field=val, ...)`` requires exactly
434- - Legacy: ``pl.struct(field=val, ...)`` — anonymous struct, fields-only.440+ one string-literal name. The parser lowers it to ``struct.create``.
435- - With name: ``pl.struct("StructName", field=val, ...)`` — explicit C++ struct441+ This runtime helper also accepts fields without a name, but that form cannot
436- type name (preferred). The first positional argument must be a string literal442+ be parsed as a kernel struct.
437- when used.
438- The runtime returns a ``SimpleNamespace``; the parser detects the call form
439- and lowers it through the new ``struct.create`` Expression op when invoked
440- inside a kernel body.
441 """443 """
442 if args:444 if args:
443 # Discard the leading name positionally at runtime; the parser uses the AST445 # Discard the leading name positionally at runtime; the parser uses the AST
@@ -268,6 +268,11 @@ def init_output(
268 automatically. Must be called before Tile buffer allocation268 automatically. Must be called before Tile buffer allocation
269 (``make_tile_group`` etc.).269 (``make_tile_group`` etc.).
270 270 
271+ The temporary UB tile starts at address 0 and occupies 8192 bytes for
272+ 8/16/32-bit types, or 512 bytes for 64-bit types, regardless of ``size``.
273+ This op does not drain MTE3 before later UB reuse; synchronize MTE3 with
274+ the subsequent reader/writer pipe before reusing that byte range.
275+ 
271 This op never emits a cross-core barrier (``pipe_barrier`` would only276 This op never emits a cross-core barrier (``pipe_barrier`` would only
272 drain the calling core's pipes). When the initialized GM data is277 drain the calling core's pipes). When the initialized GM data is
273 consumed by other cores or the cube sub-core (e.g. a subsequent278 consumed by other cores or the cube sub-core (e.g. a subsequent
@@ -407,6 +412,21 @@ def ssbuf_store(struct_var: Struct, offset: int, /) -> None:
407# — use vf.astype to convert to/from FP32/BF16/FP16 for computation).412# — use vf.astype to convert to/from FP32/BF16/FP16 for computation).
408 413 
409 414 
415+@_api_decl
416+def adds(out: Tile, lhs: Tile, rhs: Scalar) -> None:
417+ """Elementwise scalar addition: ``out = lhs + rhs``."""
418+ 
419+ 
420+@_api_decl
421+def subs(out: Tile, lhs: Tile, rhs: Scalar) -> None:
422+ """Elementwise scalar subtraction: ``out = lhs - rhs``."""
423+ 
424+ 
425+@_api_decl
426+def muls(out: Tile, lhs: Tile, rhs: Scalar) -> None:
427+ """Elementwise scalar multiplication: ``out = lhs * rhs``."""
428+ 
429+ 
410@_api_decl430@_api_decl
411def add(out: Tile, lhs: Tile, rhs: Union[Tile, int, float]) -> None:431def add(out: Tile, lhs: Tile, rhs: Union[Tile, int, float]) -> None:
412 """Element-wise addition: ``out = lhs + rhs``432 """Element-wise addition: ``out = lhs + rhs``
@@ -771,7 +791,7 @@ def matmul(
771 dst_tile: Accumulator Tile (L0C, output)791 dst_tile: Accumulator Tile (L0C, output)
772 lhs_tile: Left matrix Tile (L0A)792 lhs_tile: Left matrix Tile (L0A)
773 rhs_tile: Right matrix Tile (L0B)793 rhs_tile: Right matrix Tile (L0B)
774- bias_tile: Optional positional Bias Tile (L0B Bias area, shape [1, N], row-broadcast)794+ bias_tile: Optional positional Bias Tile (BiasTable buffer, shape [1, N], row-broadcast)
775 phase: Optional — ``pl.AccPhase.Partial`` or ``pl.AccPhase.Final``795 phase: Optional — ``pl.AccPhase.Partial`` or ``pl.AccPhase.Final``
776 """796 """
777 797 
@@ -1234,7 +1254,9 @@ def fill_index(out: Tile, start: Union[int, float]) -> None:
1234 1254 
1235@_api_decl1255@_api_decl
1236def pto_assert(condition: bool, /, format_str: Optional[str] = None, *args, loc: bool = False) -> None:1256def pto_assert(condition: bool, /, format_str: Optional[str] = None, *args, loc: bool = False) -> None:
1237- """Runtime assert: abort if condition is false, optionally print error message.1257+ """Log when the runtime condition is false; execution continues without a host exception.
1258+ 
1259+ Use ``pl.trap()`` when the kernel must abort.
1238 1260 
1239 Args:1261 Args:
1240 condition: Scalar boolean condition (dtype must be BOOL)1262 condition: Scalar boolean condition (dtype must be BOOL)
@@ -170,6 +170,9 @@ class ASTParser(
170 # Keep the Expr itself as the key. A bare id(expr) does not retain the170 # Keep the Expr itself as the key. A bare id(expr) does not retain the
171 # Python IR wrapper and can be reused by an unrelated expression.171 # Python IR wrapper and can be reused by an unrelated expression.
172 self._tile_mutex_meta: dict[ir.Expr, tuple[ir.MakeTuple, ir.MakeTuple]] = {}172 self._tile_mutex_meta: dict[ir.Expr, tuple[ir.MakeTuple, ir.MakeTuple]] = {}
173+ # Slot provenance survives SSA bindings; a dynamic Tile's type alone describes slot 0.
174+ self._tile_slot_source: dict[ir.Expr, ir.Expr] = {}
175+ self._dynamic_slot_tiles: set[ir.Expr] = set()
173 self.scope_manager = ScopeManager(strict_ssa=strict_ssa)176 self.scope_manager = ScopeManager(strict_ssa=strict_ssa)
174 self._tilingkey_consts = tilingkey_consts177 self._tilingkey_consts = tilingkey_consts
175 self._datatype_consts = datatype_consts178 self._datatype_consts = datatype_consts
@@ -608,6 +611,10 @@ class ASTParser(
608 mutex_meta = self._tile_mutex_meta.get(value_expr)611 mutex_meta = self._tile_mutex_meta.get(value_expr)
609 if mutex_meta is not None:612 if mutex_meta is not None:
610 self._tile_mutex_meta[target] = mutex_meta613 self._tile_mutex_meta[target] = mutex_meta
614+ if value_expr in self._tile_slot_source:
615+ self._tile_slot_source[target] = self._tile_slot_source[value_expr]
616+ if value_expr in self._dynamic_slot_tiles:
617+ self._dynamic_slot_tiles.add(target)
611 618 
612 def _validate_tiling_params(619 def _validate_tiling_params(
613 self,620 self,
@@ -44,8 +44,10 @@ from ..._errors import (
44 InvalidShape,44 InvalidShape,
45 InvalidType,45 InvalidType,
46 InvalidVal,46 InvalidVal,
47+ MutexMismatch,
47 OutOfRange,48 OutOfRange,
48 PyptoProError,49 PyptoProError,
50+ _warn_located,
49 message_of,51 message_of,
50)52)
51from ._tuple_type_registry import TupleTypeKind53from ._tuple_type_registry import TupleTypeKind
@@ -64,6 +66,36 @@ class BufferParserMixin:
64 66 
65 # --- helpers --------------------------------------------------------------67 # --- helpers --------------------------------------------------------------
66 68 
69+ def warn_manual_mutex_aliases(self) -> None:
70+ """Check manual aliases after native AUTO validation, reusing group metadata."""
71+ if not self._auto_mutex:
72+ return
73+ seen = set()
74+ slots = []
75+ for group, (_, mutex_ids, memory) in self.tile_group_meta.items():
76+ if mutex_ids is None:
77+ continue
78+ tiles = group.type.types[self.named_fields(group).index("tiles")].types
79+ for tile, row in zip(tiles, mutex_ids.elements):
80+ if not all(isinstance(value, ir.ConstInt) for value in row.elements):
81+ continue
82+ memref = tile.memref
83+ if memref is None or memref in seen or not isinstance(memref.addr, ir.ConstInt):
84+ continue
85+ seen.add(memref)
86+ start = int(memref.addr.value)
87+ end = start + int(memref.size)
88+ ids = {int(value.value) for value in row.elements}
89+ for other_memory, lo, hi, other_ids in slots:
90+ if memory == other_memory and start < hi and lo < end and not ids.intersection(other_ids):
91+ _warn_located(
92+ f"Tile buffers in {memory.name} overlap byte ranges [{start:#x}, {end:#x}) "
93+ f"(mutex IDs {sorted(ids)}) and [{lo:#x}, {hi:#x}) (mutex IDs {sorted(other_ids)}) "
94+ "without a shared mutex ID; auto_mutex will not order their accesses to the same bytes",
95+ MutexMismatch, group.span,
96+ )
97+ slots.append((memory, start, end, ids))
98+ 
67 @staticmethod99 @staticmethod
68 def _normalize_mutex_ids(mutex_ids, span: ir.Span) -> tuple[tuple[int, ...], ...]:100 def _normalize_mutex_ids(mutex_ids, span: ir.Span) -> tuple[tuple[int, ...], ...]:
69 """Normalize mutex IDs to one fixed-size tuple per tile slot."""101 """Normalize mutex IDs to one fixed-size tuple per tile slot."""
@@ -568,6 +600,8 @@ class BufferParserMixin:
568 )600 )
569 self._tuple_idx_counter += 1601 self._tuple_idx_counter += 1
570 tile_ir = ir.GetItemExpr(tiles, idx, span)602 tile_ir = ir.GetItemExpr(tiles, idx, span)
603+ self._tile_slot_source[tile_ir] = tiles
604+ self._dynamic_slot_tiles.add(tile_ir)
571 if mutex_ids is not None:605 if mutex_ids is not None:
572 tile_id_irs = []606 tile_id_irs = []
573 for column in range(len(mutex_ids.elements[0].elements)):607 for column in range(len(mutex_ids.elements[0].elements)):
@@ -26,7 +26,16 @@ from pypto_pro.ir._utils import _is_int, _to_make_tuple
26from pypto_pro.ir.op._op_registry import _OP_REGISTRY26from pypto_pro.ir.op._op_registry import _OP_REGISTRY
27from pypto_pro.ir.op.block_ops import block_ir_op27from pypto_pro.ir.op.block_ops import block_ir_op
28 28 
29-from ..._errors import BadFd, InvalidArgument, InvalidOperation, InvalidShape, InvalidType, InvalidVal, NotSupported29+from ..._errors import (
30+ BadFd,
31+ InvalidArgument,
32+ InvalidOperation,
33+ InvalidShape,
34+ InvalidType,
35+ InvalidVal,
36+ NotSupported,
37+ current_span,
38+)
30from .. import _api as _language_api39from .. import _api as _language_api
31from .._vf_api import Vf40from .._vf_api import Vf
32from . import _argument_checker41from . import _argument_checker
@@ -1069,11 +1078,47 @@ class CallParserMixin:
1069 1078 
1070 self._check_declared_params(op_name, call, span)1079 self._check_declared_params(op_name, call, span)
1071 1080 
1072- op_func = _OP_REGISTRY.get(op_name)1081+ # Statements already publish a span; nested calls need their own argument locations.
1073- if op_func is not None:1082+ with current_span(span, self.span_tracker.argument_spans(call)):
1074- return op_func(self, call)1083+ op_func = _OP_REGISTRY.get(op_name)
1084+ if op_func is not None:
1085+ return op_func(self, call)
1086+ return self._default_op_func(op_name, call)
1075 1087 
1076- return self._default_op_func(op_name, call)1088+ def _mx_slot_addrs(self, value: ir.Expr) -> tuple[int, ...] | None:
1089+ source = self._tile_slot_source.get(value) if value in self._dynamic_slot_tiles else None
1090+ types = source.type.types if source is not None else (value.type,)
1091+ addresses = []
1092+ for tile_type in types:
1093+ memref = getattr(tile_type, "memref", None)
1094+ address = getattr(memref, "addr", None)
1095+ if not isinstance(address, ir.ConstInt):
1096+ return None
1097+ addresses.append(int(address.value))
1098+ return tuple(addresses)
1099+ 
1100+ def _check_mx_plane_slots(self, op_name: str, args: list, kwargs: dict, span: ir.Span) -> None:
1101+ """Check runtime slot layouts after operands have been parsed exactly once."""
1102+ first = 1 if op_name == "matmul_mx" else 2
1103+ for offset, role in enumerate(("scale_a", "scale_b")):
1104+ plane_index = first + 2 + offset
1105+ plane = args[plane_index] if len(args) > plane_index else kwargs[role]
1106+ data = args[first + offset]
1107+ if data not in self._dynamic_slot_tiles and plane not in self._dynamic_slot_tiles:
1108+ continue # The builder already checks the selected static addresses.
1109+ data_addrs = self._mx_slot_addrs(data)
1110+ plane_addrs = self._mx_slot_addrs(plane)
1111+ if data_addrs is None or plane_addrs is None:
1112+ continue
1113+ expected = tuple(address >> 4 for address in data_addrs)
1114+ # A fixed plane may serve several slots that alias the same data address.
1115+ if plane_addrs != expected and not (len(set(expected)) == 1 and set(plane_addrs) == set(expected)):
1116+ raise InvalidVal(
1117+ f"{op_name}: every slot of {role} must match its data slot address >> 4; "
1118+ f"got {[hex(a) for a in plane_addrs]}, expected {[hex(a) for a in expected]}",
1119+ span=span,
1120+ hint="Pair runtime data/scale selections by slot; give the scale group explicit addresses",
1121+ )
1077 1122 
1078 def _check_declared_params(self, op_name: str, call: ast.Call, span: ir.Span) -> None:1123 def _check_declared_params(self, op_name: str, call: ast.Call, span: ir.Span) -> None:
1079 """Check which parameters the call names and how many it passes, before evaluation."""1124 """Check which parameters the call names and how many it passes, before evaluation."""
@@ -1098,6 +1143,14 @@ class CallParserMixin:
1098 the same restriction.1143 the same restriction.
1099 """1144 """
1100 span = self.span_tracker.get_span(call)1145 span = self.span_tracker.get_span(call)
1146+ if self.target == ir.SectionKind.Vector and op_name in {
1147+ "system.bar_m", "system.bar_mte1", "system.bar_fix"
1148+ }:
1149+ raise NotSupported(
1150+ f"pl.{op_name}() requires a Cube section; the current target is Vector",
1151+ span=span,
1152+ hint="Place the barrier with its Cube operations; use cross-core synchronization for a handover",
1153+ )
1101 if op_name.startswith("vf."):1154 if op_name.startswith("vf."):
1102 if self.inline_vf_depth == 0:1155 if self.inline_vf_depth == 0:
1103 raise InvalidOperation(1156 raise InvalidOperation(
@@ -28,7 +28,7 @@ def inline(fn: Callable) -> Callable:
28 28 
29 warnings.warn(29 warnings.warn(
30 "@pl.inline is deprecated and will be removed. "30 "@pl.inline is deprecated and will be removed. "
31- "Use @pl.fn with type annotations, or pass the function as an annotated callable.",31+ "Call ordinary helpers inside @pl.jit; use @pl.vector_function for VF or SIMT helpers.",
32 DeprecationWarning,32 DeprecationWarning,
33 stacklevel=2,33 stacklevel=2,
34 )34 )
@@ -76,9 +76,8 @@ using namespace pto;
76"""76"""
77 77 
78 78 
79-# pl DataType -> the torch.dtype a tensor argument for that parameter must have (used to79+# pl DataType -> accepted torch storage dtypes for a tensor parameter.
80-# validate tensor args against the kernel signature).80+_PL_DTYPE_TO_TORCH: dict[str, tuple["torch.dtype", ...]] = {}
81-_PL_DTYPE_TO_TORCH: dict[str, "torch.dtype"] = {}
82 81 
83# Sanitizer GM log budget for one launch (~1GB, split across all regions).82# Sanitizer GM log budget for one launch (~1GB, split across all regions).
84# Each executing core owns one region laid out as [ctr u32, records...] and is83# Each executing core owns one region laid out as [ctr u32, records...] and is
@@ -98,21 +97,42 @@ def _register_pl_dtype(pl_dtype: DataType, torch_name: str) -> None:
98 """Map a pl dtype to the torch dtype named ``torch_name``, if this torch build has it."""97 """Map a pl dtype to the torch dtype named ``torch_name``, if this torch build has it."""
99 torch_dtype = getattr(torch, torch_name, None)98 torch_dtype = getattr(torch, torch_name, None)
100 if torch_dtype is not None:99 if torch_dtype is not None:
101- _PL_DTYPE_TO_TORCH[str(pl_dtype)] = torch_dtype100+ key = str(pl_dtype)
101+ _PL_DTYPE_TO_TORCH[key] = (*_PL_DTYPE_TO_TORCH.get(key, ()), torch_dtype)
102 102 
103 103 
104for _pl_dtype, _torch_name in (104for _pl_dtype, _torch_name in (
105 (DataType.FP16, "float16"),105 (DataType.FP16, "float16"),
106 (DataType.BF16, "bfloat16"),106 (DataType.BF16, "bfloat16"),
107 (DataType.FP32, "float32"),107 (DataType.FP32, "float32"),
108+ (DataType.FP64, "float64"),
109+ (DataType.INT4, "uint8"),
108 (DataType.INT8, "int8"),110 (DataType.INT8, "int8"),
109 (DataType.INT16, "int16"),111 (DataType.INT16, "int16"),
112+ (DataType.INT16, "uint16"),
110 (DataType.INT32, "int32"),113 (DataType.INT32, "int32"),
111 (DataType.INT64, "int64"),114 (DataType.INT64, "int64"),
115+ (DataType.UINT4, "uint8"),
112 (DataType.UINT8, "uint8"),116 (DataType.UINT8, "uint8"),
117+ (DataType.UINT16, "uint16"),
118+ (DataType.UINT16, "int16"),
119+ (DataType.UINT32, "uint32"),
120+ (DataType.UINT32, "int32"),
113 (DataType.UINT64, "uint64"),121 (DataType.UINT64, "uint64"),
114 (DataType.BOOL, "bool"),122 (DataType.BOOL, "bool"),
123+ (DataType.FP4, "uint8"),
124+ (DataType.FP4E2M1, "uint8"),
125+ (DataType.FP4E2M1, "float4_e2m1fn_x2"),
126+ (DataType.FP4E1M2, "uint8"),
127+ (DataType.FP8E4M3FN, "uint8"),
128+ (DataType.FP8E4M3FN, "float8_e4m3fn"),
129+ (DataType.FP8E5M2, "uint8"),
130+ (DataType.FP8E5M2, "float8_e5m2"),
131+ (DataType.FP8E8M0, "uint8"),
132+ (DataType.FP8E8M0, "float8_e8m0fnu"),
133+ (DataType.HF4, "uint8"),
115 (DataType.HF8, "uint8"),134 (DataType.HF8, "uint8"),
135+ (DataType.INDEX, "int64"),
116):136):
117 _register_pl_dtype(_pl_dtype, _torch_name)137 _register_pl_dtype(_pl_dtype, _torch_name)
118 138 
@@ -466,8 +486,8 @@ def write_datatype_metadata(output_dir: str, metadata: dict | None) -> None:
466def _pack_factor(dtype: DataType | None) -> int:486def _pack_factor(dtype: DataType | None) -> int:
467 """How many elements of ``dtype`` torch reports as one storage unit along the packed axis.487 """How many elements of ``dtype`` torch reports as one storage unit along the packed axis.
468 488 
469- A sub-byte dtype has no torch dtype of its own, so callers pass the packed storage buffer489+ Callers pass sub-byte arguments as a packed storage buffer (uint8 or a compatible packed
470- (uint8, two fp4 per byte) and torch reports its shape in those storage units. pypto counts490+ torch dtype, two fp4 per byte) and torch reports its shape in storage units. pypto counts
471 elements everywhere, so the packed axis -- the innermost one, the only axis a sub-byte491 elements everywhere, so the packed axis -- the innermost one, the only axis a sub-byte
472 element can be packed along -- has to be scaled back up before a tensor argument is492 element can be packed along -- has to be scaled back up before a tensor argument is
473 compared against its ParamSpec or fed to the dynamic ABI tail. Whole-byte dtypes are493 compared against its ParamSpec or fed to the dynamic ABI tail. Whole-byte dtypes are
@@ -482,7 +502,7 @@ def _pack_factor(dtype: DataType | None) -> int:
482 502 
483 503 
484def _pl_dtype_to_torch(dtype: DataType):504def _pl_dtype_to_torch(dtype: DataType):
485- """Return the torch.dtype an argument for this pl DataType must have, or None if unknown."""505+ """Return accepted torch storage dtypes for this pl DataType, or None if unknown."""
486 return _PL_DTYPE_TO_TORCH.get(str(dtype))506 return _PL_DTYPE_TO_TORCH.get(str(dtype))
487 507 
488 508 
@@ -650,7 +670,7 @@ def _pack_tiling_arg(args: tuple) -> tuple:
650class _LaunchPlan:670class _LaunchPlan:
651 """Everything about a launch that depends only on the kernel's ParamSpecs.671 """Everything about a launch that depends only on the kernel's ParamSpecs.
652 672 
653- Which parameter is a tensor, what torch dtype it must carry, which dimensions are673+ Which parameter is a tensor, what torch dtypes it may carry, which dimensions are
654 compile-time constants, which are dynamic ABI variables, and the order those variables674 compile-time constants, which are dynamic ABI variables, and the order those variables
655 are appended in: none of that can change once the kernel is compiled, so it is resolved675 are appended in: none of that can change once the kernel is compiled, so it is resolved
656 here exactly once and the hot path just walks flat tuples.676 here exactly once and the hot path just walks flat tuples.
@@ -659,7 +679,7 @@ class _LaunchPlan:
659 n_params: Expected argument count.679 n_params: Expected argument count.
660 has_tiling: Whether any parameter is a tiling struct (skips _pack_tiling_arg).680 has_tiling: Whether any parameter is a tiling struct (skips _pack_tiling_arg).
661 slots: Per-parameter descriptors in ABI order. Tensor slots are681 slots: Per-parameter descriptors in ABI order. Tensor slots are
662- ``(True, index, name, torch_dtype | None, rank, const_dims, dyn_dims, pack)``682+ ``(True, index, name, torch_dtypes | None, rank, const_dims, dyn_dims, pack)``
663 where ``rank`` is -1 for parameters whose shape is not checked (PTR, or no683 where ``rank`` is -1 for parameters whose shape is not checked (PTR, or no
664 declared shape), ``dyn_dims`` lists the dimensions this parameter contributes684 declared shape), ``dyn_dims`` lists the dimensions this parameter contributes
665 to the dynamic ABI tail, and ``pack`` is the sub-byte packing factor from685 to the dynamic ABI tail, and ``pack`` is the sub-byte packing factor from
@@ -798,7 +818,7 @@ def _build_launch_entry(compiled: "CompiledKernel"):
798 _validate_scalar_arg(index, arg, slot[2])818 _validate_scalar_arg(index, arg, slot[2])
799 append(arg)819 append(arg)
800 continue820 continue
801- _, _, name, expected_dtype, rank, consts, dyn_dims, pack = slot821+ _, _, name, expected_dtypes, rank, consts, dyn_dims, pack = slot
802 if arg is None:822 if arg is None:
803 append(None) # ctypes maps None to a null void*823 append(None) # ctypes maps None to a null void*
804 if dyn_dims:824 if dyn_dims:
@@ -807,10 +827,10 @@ def _build_launch_entry(compiled: "CompiledKernel"):
807 if not isinstance(arg, torch.Tensor):827 if not isinstance(arg, torch.Tensor):
808 raise InvalidType(f"arg[{index}] '{name}': expected torch.Tensor, got {type(arg).__name__}")828 raise InvalidType(f"arg[{index}] '{name}': expected torch.Tensor, got {type(arg).__name__}")
809 if rank >= 0:829 if rank >= 0:
810- # torch.dtype values are singletons, so identity is enough and cheaper.830+ if expected_dtypes is not None and arg.dtype not in expected_dtypes:
811- if expected_dtype is not None and arg.dtype is not expected_dtype:831+ expected = " or ".join(str(dtype) for dtype in expected_dtypes)
812 raise InvalidType(832 raise InvalidType(
813- f"arg[{index}] '{name}': dtype mismatch — expected {expected_dtype}, got {arg.dtype}"833+ f"arg[{index}] '{name}': dtype mismatch — expected {expected}, got {arg.dtype}"
814 )834 )
815 # One .shape per tensor: reused for the positivity check, the rank check,835 # One .shape per tensor: reused for the positivity check, the rank check,
816 # the constant-dimension checks and this parameter's slice of the dynamic836 # the constant-dimension checks and this parameter's slice of the dynamic
@@ -2307,11 +2327,12 @@ class _TileJitKernel:
2307 raise InvalidType(2327 raise InvalidType(
2308 f"Tensor parameter '{tensor_spec.parameter_name}' expected torch.Tensor, got {type(arg).__name__}"2328 f"Tensor parameter '{tensor_spec.parameter_name}' expected torch.Tensor, got {type(arg).__name__}"
2309 )2329 )
2310- expected_dtype = _pl_dtype_to_torch(tensor_spec.dtype)2330+ expected_dtypes = _pl_dtype_to_torch(tensor_spec.dtype)
2311- if expected_dtype is not None and arg.dtype != expected_dtype:2331+ if expected_dtypes is not None and arg.dtype not in expected_dtypes:
2332+ expected = " or ".join(str(dtype) for dtype in expected_dtypes)
2312 raise InvalidType(2333 raise InvalidType(
2313 f"Tensor parameter '{tensor_spec.parameter_name}' dtype mismatch: "2334 f"Tensor parameter '{tensor_spec.parameter_name}' dtype mismatch: "
2314- f"expected {expected_dtype}, got {arg.dtype}"2335+ f"expected {expected}, got {arg.dtype}"
2315 )2336 )
2316 return bound_signature2337 return bound_signature
2317 2338 
@@ -255,8 +255,15 @@ class KernelDef:
255 self,255 self,
256 target: ir.SectionKind,256 target: ir.SectionKind,
257 bound_signature=None,257 bound_signature=None,
258+ *,
259+ observer=None,
poorwill
poorwillpoorwill27 days ago

这个参数咋没看到地方用到

likedislike
ddddwe
27 days ago Comments:
258 ) -> tuple[ir.Program, bool]:260 ) -> tuple[ir.Program, bool]:
259- """Parse a fresh target Program and report whether its target section was matched."""261+ """Parse a fresh target Program and report whether its target section was matched.
262+ 
263+ The invocation-local ``observer(program, parser)`` runs after AUTO mutex ID
264+ resolution. Parser tables retain their parse-time metadata. The observer
265+ must not mutate either object; its exceptions propagate to the caller.
266+ """
260 program_name = self._name if self._name is not None else self._func.__name__267 program_name = self._name if self._name is not None else self._func.__name__
261 268 
262 try:269 try:
@@ -353,6 +360,9 @@ class KernelDef:
353 str(exc),360 str(exc),
354 span=span_of_spec_message(str(exc)) or mutex_id_manager.diagnostic_span,361 span=span_of_spec_message(str(exc)) or mutex_id_manager.diagnostic_span,
355 ) from None362 ) from None
363+ parser.warn_manual_mutex_aliases()
364+ if observer is not None:
365+ observer(program, parser)
356 return program, parser.matched_target366 return program, parser.matched_target
357 367 
358 except PyptoProError as e:368 except PyptoProError as e:
@@ -404,6 +404,17 @@ class KernelSignatureSpec:
404 raise InvalidType(404 raise InvalidType(
405 f"Tensor parameter '{tensor.parameter_name}' must provide a shape, got {type(value).__name__}"405 f"Tensor parameter '{tensor.parameter_name}' must provide a shape, got {type(value).__name__}"
406 )406 )
407+ if (
408+ tensor.dtype is not None
409+ and tensor.dtype.get_bit() == 4
410+ and isinstance(shape, Sequence)
411+ and not isinstance(shape, (str, bytes))
412+ and shape
413+ ):
414+ last = _validate_positive_int(
415+ shape[-1], tensor.parameter_name, len(shape) - 1, source="runtime dimension"
416+ )
417+ shape = tuple(shape[:-1]) + (last * 2,)
407 shapes[tensor.parameter_name] = shape418 shapes[tensor.parameter_name] = shape
408 return self.bind_static_shapes(shapes)419 return self.bind_static_shapes(shapes)
409 420 
@@ -93,6 +93,19 @@ py::object AnyToPyObject(const std::any& value, const std::string& key)
93 throw pypto::ir::TypeError("Attribute '" + key + "' has unsupported type");93 throw pypto::ir::TypeError("Attribute '" + key + "' has unsupported type");
94}94}
95 95 
96+// Snapshot all attributes, including list/expression metadata. Unknown types must not disappear.
97+py::dict ReadAttributes(const std::vector<std::pair<std::string, std::any>>& attrs)
poorwill
poorwillpoorwill27 days ago

好像没找到使用的地方

likedislike
ddddwe
27 days ago Comments:
98+{
99+ py::dict result;
100+ for (const auto& [key, value] : attrs) {
101+ result[py::str(key)] = AnyToPyObject<int, int64_t, uint64_t, bool, float, double, std::string, DataType,
102+ MemorySpace, TilePad, TensorLayout, PipeType, ExprPtr, std::vector<int>,
103+ std::vector<int64_t>, std::vector<std::string>, std::vector<ExprPtr>>(
104+ value, key);
105+ }
106+ return result;
107+}
108+ 
96template <typename T>109template <typename T>
97std::vector<T> ConvertSequenceToVector(py::handle obj)110std::vector<T> ConvertSequenceToVector(py::handle obj)
98{111{
@@ -1540,6 +1553,17 @@ void BindProgram(py::module_& ir)
1540 1553 
1541void BindHelpers(py::module_& ir)1554void BindHelpers(py::module_& ir)
1542{1555{
1556+ ir.attr("attributes_api_version") = 1;
1557+ ir.def(
1558+ "get_attributes", [](const CallPtr& node) { return ReadAttributes(node->kwargs_); }, py::arg("node"),
1559+ "Return every Call attribute; unsupported types raise TypeError");
1560+ ir.def(
1561+ "get_attributes", [](const FunctionPtr& node) { return ReadAttributes(node->attrs_); }, py::arg("node"),
1562+ "Return every Function attribute; unsupported types raise TypeError");
1563+ ir.def(
1564+ "get_attributes", [](const ForStmtPtr& node) { return ReadAttributes(node->attrs_); }, py::arg("node"),
1565+ "Return every ForStmt attribute; unsupported types raise TypeError");
1566+ 
1543 // operator functions for Var (wrapped in Python for span capture and normalization)1567 // operator functions for Var (wrapped in Python for span capture and normalization)
1544 ir.def("add", &MakeAdd, py::arg("lhs"), py::arg("rhs"), py::arg("span") = Span::Unknown(), "Addition operator");1568 ir.def("add", &MakeAdd, py::arg("lhs"), py::arg("rhs"), py::arg("span") = Span::Unknown(), "Addition operator");
1545 ir.def("sub", &MakeSub, py::arg("lhs"), py::arg("rhs"), py::arg("span") = Span::Unknown(), "Subtraction operator");1569 ir.def("sub", &MakeSub, py::arg("lhs"), py::arg("rhs"), py::arg("span") = Span::Unknown(), "Subtraction operator");
@@ -244,14 +244,16 @@ def _barrier_kernel(
244 x: pl.Tensor[[pl.DYNAMIC, pl.DYNAMIC], pl.DT_FP32],244 x: pl.Tensor[[pl.DYNAMIC, pl.DYNAMIC], pl.DT_FP32],
245 condition: pl.DT_BOOL,245 condition: pl.DT_BOOL,
246):246):
247- with pl.section_vector():247+ with pl.section_cube():
248 for _ in pl.range(0, x.shape[0]):248 for _ in pl.range(0, x.shape[0]):
249 pl.system.bar_mte1()249 pl.system.bar_mte1()
250- pl.system.bar_mte2()
251- pl.system.bar_mte3()
252 if condition:250 if condition:
253 pl.system.bar_m()251 pl.system.bar_m()
254 pl.system.bar_fix()252 pl.system.bar_fix()
253+ with pl.section_vector():
254+ for _ in pl.range(0, x.shape[0]):
255+ pl.system.bar_mte2()
256+ pl.system.bar_mte3()
255 pl.system.bar_all()257 pl.system.bar_all()
256 pl.system.bar_all()258 pl.system.bar_all()
257 pl.system.sync_src(set_pipe=pl.PipeType.MTE2, wait_pipe=pl.PipeType.V, event_id=0)259 pl.system.sync_src(set_pipe=pl.PipeType.MTE2, wait_pipe=pl.PipeType.V, event_id=0)
@@ -234,8 +234,9 @@ def test_wrong_positional_count_raises():
234 t = pl.make_tile(tt, addr=0)234 t = pl.make_tile(tt, addr=0)
235 pl.load(pl.reinterpret(t, t, shape=[64, 64], dtype=pl.DT_BF16), x, [0, 0])235 pl.load(pl.reinterpret(t, t, shape=[64, 64], dtype=pl.DT_BF16), x, [0, 0])
236 236 
237- with pytest.raises(InvalidArgument, match="exactly 1 positional argument"):237+ with pytest.raises(InvalidArgument) as caught:
238 _parse_kernel(k)238 _parse_kernel(k)
239+ assert "too many positional arguments" in caught.value.message
239 240 
240 241 
241def test_dtype_without_shape_rejected_in_kernel():242def test_dtype_without_shape_rejected_in_kernel():
@@ -314,9 +315,10 @@ def test_reinterpret_inside_nested_loops():
314 tt = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)315 tt = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)
315 g = pl.make_tile_group(type=tt, addrs=[0x0000, 0x2000], mutex_ids=[0, 1])316 g = pl.make_tile_group(type=tt, addrs=[0x0000, 0x2000], mutex_ids=[0, 1])
316 td = pl.DT_BF16317 td = pl.DT_BF16
318+ view = pl.reinterpret(g, shape=[shape_rows, shape_cols], dtype=td)
317 for i in pl.range(0, a.shape[0], 64):319 for i in pl.range(0, a.shape[0], 64):
318 for j in pl.range(0, a.shape[1], 64):320 for j in pl.range(0, a.shape[1], 64):
319- t2 = pl.reinterpret(g.next(), shape=[shape_rows, shape_cols], dtype=td)321+ t2 = view.next()
320 pl.load_tile(t2, a, [i, j])322 pl.load_tile(t2, a, [i, j])
321 pl.store_tile(out, t2, [i, j])323 pl.store_tile(out, t2, [i, j])
322 324 
@@ -482,7 +484,7 @@ def test_slot_tile_reinterpret_keeps_mutex_binding():
482 def k(a: pl.Tensor[[64, 64], pl.DT_FP16]):484 def k(a: pl.Tensor[[64, 64], pl.DT_FP16]):
483 tt = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)485 tt = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)
484 db = pl.make_tile_group(type=tt, addrs=[0, 0x4000], mutex_ids=[5, 6])486 db = pl.make_tile_group(type=tt, addrs=[0, 0x4000], mutex_ids=[5, 6])
485- t2 = pl.reinterpret(db.current(), shape=[64, 64], dtype=pl.DT_BF16)487+ t2 = pl.reinterpret(db[1], shape=[64, 64], dtype=pl.DT_BF16)
486 pl.load(t2, a, [0, 0])488 pl.load(t2, a, [0, 0])
487 489 
488 ir_str = _kernel_ir(k)490 ir_str = _kernel_ir(k)
@@ -0,0 +1,52 @@
1+#!/usr/bin/env python3
2+# coding: utf-8
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+"""Attribute snapshots retain supported metadata and reject unsupported values."""
12+ 
13+from pypto_pro import ir
14+import pytest
15+ 
16+import pypto
17+ 
18+ 
19+def test_call_snapshot_retains_lists_and_is_read_only():
20+ node = ir.Call("attribute_probe", [], {"ids": [2, 5], "fields": ["cursor", "tiles"],
21+ "space": ir.MemorySpace.Vec, "enabled": True, "scale": 1.25},
22+ ir.Span.unknown())
23+ attrs = ir.get_attributes(node)
24+ assert ir.attributes_api_version == 1
25+ assert attrs == {"ids": [2, 5], "fields": ["cursor", "tiles"],
26+ "space": ir.MemorySpace.Vec, "enabled": True, "scale": 1.25}
27+ attrs["ids"].append(9)
28+ assert ir.get_attributes(node)["ids"] == [2, 5]
29+ 
30+ 
31+def test_function_and_for_snapshots_retain_list_metadata():
32+ span = ir.Span.unknown()
33+ value = ir.ConstInt(2, ir.INT64, span)
34+ function = ir.Function("probe", [], [], ir.SeqStmts(span), span,
35+ attrs={"ids": [2, 5], "label": "cursor"})
36+ assert ir.get_attributes(function) == {"ids": [2, 5], "label": "cursor"}
37+ loop = ir.ForStmt(ir.Var("i", ir.ScalarType(ir.INT64), span), value, value, value,
38+ [], ir.SeqStmts(span), [], span, attrs={"ids": [1, 4]})
39+ assert ir.get_attributes(loop) == {"ids": [1, 4]}
40+ 
41+ 
42+def test_unknown_attribute_type_fails_with_its_key():
43+ span = ir.Span.unknown()
44+ node = ir.Function("probe", [], [], ir.SeqStmts(span), span,
45+ attrs={"symbolic": pypto.SymbolicScalar("extent")})
46+ with pytest.raises(TypeError, match="symbolic.*unsupported type"):
47+ ir.get_attributes(node)
48+ 
49+ 
50+def test_unsupported_node_type_fails():
51+ with pytest.raises(TypeError):
52+ ir.get_attributes(ir.ConstInt(1, ir.INT64, ir.Span.unknown()))
@@ -0,0 +1,49 @@
1+#!/usr/bin/env python3
2+# coding: utf-8
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+"""Tile type equality compares buffer kind while keeping allocation identity separate."""
12+ 
13+from pypto_pro import DataType, ir
14+import pytest
15+ 
16+ 
17+def _tile(space=None, address=0, dtype=DataType.INT64):
18+ span = ir.Span.unknown()
19+ dims = [ir.ConstInt(d, DataType.INDEX, span) for d in (1, 64)]
20+ memref = ir.MemRef(space, ir.ConstInt(address, DataType.INDEX, span), 512, span) if space else None
21+ return ir.TileType(dims, dtype, memref) if memref is not None else ir.TileType(dims, dtype)
22+ 
23+ 
24+def test_distinct_buffers_have_distinct_types():
25+ assert not ir.structural_equal(_tile(ir.MemorySpace.Mat), _tile(ir.MemorySpace.Scaling))
26+ 
27+ 
28+def test_addresses_are_not_part_of_the_type():
29+ assert ir.structural_equal(_tile(ir.MemorySpace.Mat), _tile(ir.MemorySpace.Mat, 4096))
30+ 
31+ 
32+@pytest.mark.parametrize("space", [ir.MemorySpace.Mat, ir.MemorySpace.Vec,
33+ ir.MemorySpace.ScaleLeft, ir.MemorySpace.ScaleRight])
34+@pytest.mark.parametrize("reverse", [False, True])
35+def test_known_and_unspecified_memory_spaces_are_not_equal(space, reverse):
36+ lhs, rhs = _tile(space), _tile()
37+ if reverse:
38+ lhs, rhs = rhs, lhs
39+ assert not ir.structural_equal(lhs, rhs)
40+ with pytest.raises(RuntimeError, match="memory reference presence mismatch"):
41+ ir.assert_structural_equal(lhs, rhs)
42+ 
43+ 
44+def test_both_unspecified_memory_spaces_are_equal():
45+ assert ir.structural_equal(_tile(), _tile())
46+ 
47+ 
48+def test_dtype_still_participates_in_equality():
49+ assert not ir.structural_equal(_tile(ir.MemorySpace.Mat), _tile(ir.MemorySpace.Mat, dtype=DataType.FP16))
@@ -1057,5 +1057,6 @@ def test_make_tile_group_rejects_wrong_mutex_ids_container_type(mutex_ids):
1057 tt = pl.TileType(shape=[32], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)1057 tt = pl.TileType(shape=[32], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)
1058 pl.make_tile_group(type=tt, addrs=0, mutex_ids=mutex_ids)1058 pl.make_tile_group(type=tt, addrs=0, mutex_ids=mutex_ids)
1059 1059 
1060- with pytest.raises(InvalidType, match="mutex_ids must be a list, tuple, or None"):1060+ with pytest.raises(InvalidType) as caught:
1061 _parse_kernel(k)1061 _parse_kernel(k)
1062+ assert 'mutex_ids must be "auto", a list, tuple, or None' in caught.value.message
@@ -0,0 +1,70 @@
1+#!/usr/bin/env python3
2+# coding: utf-8
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+"""Public scalar aliases and core-side barriers have complete DSL contracts."""
12+ 
13+from pypto_pro import ir
14+from pypto_pro._errors import InvalidOperation, NotSupported
15+import pypto_pro.language as pl
16+import pytest
17+ 
18+ 
19+@pl.jit(arch="3510", auto_mutex=False)
20+def scalar_aliases(x: pl.Tensor[[1], pl.DT_FP32]):
21+ tt = pl.TileType(shape=[1, 64], dtype=pl.DT_FP32, target_memory=pl.MemorySpace.Vec)
22+ a = pl.make_tile(tt, addr=0)
23+ b = pl.make_tile(tt, addr=256)
24+ with pl.section_vector():
25+ pl.adds(b, a, 0.5)
26+ pl.subs(b, b, 0.25)
27+ pl.muls(b, b, 2.0)
28+ 
29+ 
30+def test_public_aliases_parse_and_export():
31+ program, _ = scalar_aliases.to_kernel_def().parse_target_program(ir.SectionKind.Vector)
32+ text = ir.python_print(program)
33+ for name in ("adds", "subs", "muls"):
34+ assert name in pl.__all__ and callable(getattr(pl, name))
35+ assert "block." + name in text
36+ with pytest.raises(InvalidOperation):
37+ getattr(pl, name)(None, None, 1)
38+ 
39+ 
40+@pl.jit(arch="3510", auto_mutex=False)
41+def cube_barriers(x: pl.Tensor[[1], pl.DT_FP32]):
42+ with pl.section_cube():
43+ pl.system.bar_m()
44+ pl.system.bar_mte1()
45+ pl.system.bar_fix()
46+ 
47+ 
48+def _vector_barrier(which):
49+ @pl.jit(arch="3510", auto_mutex=False)
50+ def kernel(x: pl.Tensor[[1], pl.DT_FP32]):
51+ with pl.section_vector():
52+ if which == 0:
53+ pl.system.bar_m()
54+ elif which == 1:
55+ pl.system.bar_mte1()
56+ else:
57+ pl.system.bar_fix()
58+ return kernel
59+ 
60+ 
61+def test_cube_barriers_remain_supported():
62+ assert cube_barriers.to_kernel_def().parse_target_program(ir.SectionKind.Cube)[1]
63+ 
64+ 
65+@pytest.mark.parametrize("which", [0, 1, 2])
66+def test_cube_only_barriers_reject_vector_target_with_location(which):
67+ with pytest.raises(NotSupported) as caught:
68+ _vector_barrier(which).to_kernel_def().parse_target_program(ir.SectionKind.Vector)
69+ assert "requires a Cube section" in caught.value.message
70+ assert caught.value.span["filename"] == __file__
@@ -346,8 +346,9 @@ def test_sync_all_rejects_workspaces_in_parser():
346 def func(_jit_entry: pl.DT_INT64):346 def func(_jit_entry: pl.DT_INT64):
347 pl.system.sync_all([0])347 pl.system.sync_all([0])
348 348 
349- with pytest.raises(InvalidArgument, match="does not accept positional arguments"):349+ with pytest.raises(InvalidArgument) as caught:
350 func.to_kernel_def().parse_target_program(ir.SectionKind.Vector)350 func.to_kernel_def().parse_target_program(ir.SectionKind.Vector)
351+ assert "too many positional arguments" in caught.value.message
351 352 
352 353 
353def test_complex_integer_event_id_expression_is_accepted():354def test_complex_integer_event_id_expression_is_accepted():
@@ -0,0 +1,125 @@
1+#!/usr/bin/env python3
2+# coding: utf-8
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+"""Slot provenance protects dynamic aliases and every MX plane address."""
12+ 
13+from pypto_pro import ir
14+from pypto_pro._errors import InvalidVal, NotSupported
15+import pypto_pro.language as pl
16+import pytest
17+ 
18+ 
19+def _parse(kernel):
20+ return kernel.to_kernel_def().parse_target_program(ir.SectionKind.Cube)
21+ 
22+ 
23+def _reinterpret_kernel(selection, depth=2):
24+ @pl.jit(arch="3510", auto_mutex=False)
25+ def kernel(index: pl.DT_INT32):
26+ tt = pl.TileType(shape=[16, 16], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Mat)
27+ group = pl.make_tile_group(type=tt, addrs=0, depth=depth)
28+ with pl.section_cube():
29+ if selection == 0:
30+ tile = group.next()
31+ alias = tile
32+ view = pl.reinterpret(alias, shape=[16, 16])
33+ elif selection == 1:
34+ view = pl.reinterpret(group[index], shape=[16, 16])
35+ elif selection == 2:
36+ view = pl.reinterpret(group[1], shape=[16, 16])
37+ else:
38+ view = pl.reinterpret(group, shape=[16, 16])
39+ tile = view.next()
40+ return kernel
41+ 
42+ 
43+@pytest.mark.parametrize("selection", [0, 1])
44+def test_dynamic_slot_reinterpret_is_rejected(selection):
45+ with pytest.raises(NotSupported) as caught:
46+ _parse(_reinterpret_kernel(selection))
47+ assert "runtime slot index" in caught.value.message
48+ 
49+ 
50+@pytest.mark.parametrize("selection,depth", [(0, 1), (2, 2), (3, 2)])
51+def test_static_single_slot_and_group_aliases_remain_supported(selection, depth):
52+ _parse(_reinterpret_kernel(selection, depth))
53+ 
54+ 
55+def _mx_kernel(plane_addrs, mode=0, plane_depth=2, data_index=0, plane_index=0,
56+ data_addrs=0, accumulate=False):
57+ @pl.jit(arch="3510", auto_mutex=False)
58+ def kernel(x: pl.Tensor[[1], pl.DT_FP32], index: pl.DT_INT32):
59+ at = pl.TileType(shape=[64, 64], dtype=pl.DT_FP8E4M3FN, target_memory=pl.MemorySpace.Left)
60+ bt = pl.TileType(shape=[64, 64], dtype=pl.DT_FP8E4M3FN, target_memory=pl.MemorySpace.Right)
61+ sat = pl.TileType(shape=[64, 2], dtype=pl.DT_FP8E8M0, target_memory=pl.MemorySpace.ScaleLeft)
62+ sbt = pl.TileType(shape=[2, 64], dtype=pl.DT_FP8E8M0, target_memory=pl.MemorySpace.ScaleRight)
63+ ct = pl.TileType(shape=[64, 64], dtype=pl.DT_FP32, target_memory=pl.MemorySpace.Acc)
64+ a = pl.make_tile_group(type=at, addrs=data_addrs, depth=2)
65+ b = pl.make_tile_group(type=bt, addrs=0, depth=2)
66+ sa = pl.make_tile_group(type=sat, addrs=plane_addrs, depth=plane_depth)
67+ sb = pl.make_tile_group(type=sbt, addrs=[0, 256], depth=2)
68+ c = pl.make_tile_group(type=ct, addrs=0, depth=1)
69+ with pl.section_cube():
70+ if mode == 1:
71+ pl.matmul_mx(c.current(), a.current(), b.current(),
72+ scale_a=sa.current(), scale_b=sb.current())
73+ elif mode == 2:
74+ pl.matmul_mx(c[0], a[data_index], b[data_index], sa[plane_index], sb[data_index])
75+ elif mode == 3:
76+ pl.matmul_mx(c[0], a[index], b[index], sa[index], sb[index])
77+ else:
78+ av = a.current()
79+ bv = b.current()
80+ sav = sa.current()
81+ sbv = sb.current()
82+ if accumulate:
83+ pl.matmul_mx_acc(c.current(), c.current(), av, bv, sav, sbv)
84+ else:
85+ pl.matmul_mx(c.current(), av, bv, sav, sbv)
86+ return kernel
87+ 
88+ 
89+@pytest.mark.parametrize("mode", [0, 1, 3])
90+def test_every_mx_plane_slot_is_checked(mode):
91+ _parse(_mx_kernel([0, 256], mode))
92+ with pytest.raises(InvalidVal) as caught:
93+ _parse(_mx_kernel(0, mode))
94+ assert "every slot of scale_a" in caught.value.message
95+ 
96+ 
97+def test_mx_plane_requires_one_slot_per_data_slot():
98+ with pytest.raises(InvalidVal) as caught:
99+ _parse(_mx_kernel([0], plane_depth=1))
100+ assert "every slot of scale_a" in caught.value.message
101+ 
102+ 
103+def test_static_mx_selection_does_not_validate_unused_slots():
104+ _parse(_mx_kernel(0, mode=2))
105+ 
106+ 
107+def test_static_mx_pair_can_use_different_group_depths_and_indices():
108+ _parse(_mx_kernel([256], mode=2, plane_depth=1, data_index=1))
109+ 
110+ 
111+def test_static_mx_selection_still_checks_its_selected_address():
112+ with pytest.raises(InvalidVal) as caught:
113+ _parse(_mx_kernel(0, mode=2, data_index=1, plane_index=1))
114+ assert "scale_a address must equal lhs_tile address >> 4" in caught.value.message
115+ 
116+ 
117+def test_fixed_mx_plane_can_serve_runtime_slots_aliasing_one_address():
118+ _parse(_mx_kernel([0], plane_depth=1, data_addrs=[0, 0]))
119+ 
120+ 
121+def test_mx_acc_checks_runtime_plane_layout():
122+ _parse(_mx_kernel([0, 256], accumulate=True))
123+ with pytest.raises(InvalidVal) as caught:
124+ _parse(_mx_kernel(0, accumulate=True))
125+ assert "every slot of scale_a" in caught.value.message
@@ -22,6 +22,7 @@ import sys
22from unittest.mock import MagicMock22from unittest.mock import MagicMock
23 23 
24from pypto_pro._errors import InvalidShape24from pypto_pro._errors import InvalidShape
25+from pypto_pro.language import STATIC, Tensor
25from pypto_pro.runtime.platform import NpuArch26from pypto_pro.runtime.platform import NpuArch
26import pytest27import pytest
27 28 
@@ -522,12 +523,79 @@ def test_args_to_ctypes_no_dyn(launch):
522# Sub-byte dtypes (packed storage)523# Sub-byte dtypes (packed storage)
523# ---------------------------------------------------------------------------524# ---------------------------------------------------------------------------
524#525#
525-# A sub-byte dtype has no torch dtype of its own, so the caller hands over a uint8526+# A sub-byte argument uses uint8 or a compatible packed Torch dtype, holding two
526-# buffer holding two fp4 per byte and torch reports the innermost axis in those527+# fp4 per byte, and torch reports the innermost axis in those
527# storage units. ParamSpec and the kernel ABI count elements, so the launch plan528# storage units. ParamSpec and the kernel ABI count elements, so the launch plan
528# resolves a packing factor per parameter and the launch path scales that one axis.529# resolves a packing factor per parameter and the launch path scales that one axis.
529 530 
530 531 
532+@pytest.fixture
533+def validate_carrier(launch):
534+ def _validate(stage, dtype, carrier):
535+ if carrier is None:
536+ pytest.skip("This Torch build does not provide the carrier dtype")
537+ arg = _MockTensor([1, 32], carrier)
538+ if stage == "compile":
539+ def kernel(x: Tensor[[1, STATIC], dtype]):
540+ pass
541+ _jit._TileJitKernel(kernel, arch=NpuArch.DAV_3510)._bind_runtime_args((arg,))
542+ else:
543+ launch([_tensor_spec("x", dtype, [1, "K"])], (arg,))
544+ return _validate
545+ 
546+ 
547+@pytest.mark.parametrize("stage", ["compile", "launch"])
548+@pytest.mark.parametrize("dtype,carrier", [
549+ (DataType.INT4, torch.uint8),
550+ (DataType.UINT4, torch.uint8),
551+ (DataType.FP4, torch.uint8),
552+ (DataType.FP4E2M1, torch.uint8),
553+ (DataType.FP4E1M2, torch.uint8),
554+ (DataType.HF4, torch.uint8),
555+ (DataType.FP4E2M1, getattr(torch, "float4_e2m1fn_x2", None)),
556+ (DataType.FP8E4M3FN, torch.uint8),
557+ (DataType.FP8E4M3FN, getattr(torch, "float8_e4m3fn", None)),
558+ (DataType.FP8E5M2, torch.uint8),
559+ (DataType.FP8E5M2, getattr(torch, "float8_e5m2", None)),
560+ (DataType.FP8E8M0, torch.uint8),
561+ (DataType.FP8E8M0, getattr(torch, "float8_e8m0fnu", None)),
562+ (DataType.UINT16, getattr(torch, "uint16", None)),
563+ (DataType.UINT16, torch.int16),
564+ (DataType.INT16, getattr(torch, "uint16", None)),
565+ (DataType.UINT32, getattr(torch, "uint32", None)),
566+ (DataType.UINT32, torch.int32),
567+ (DataType.FP64, torch.float64),
568+ (DataType.INDEX, torch.int64),
569+])
570+def test_tensor_carriers_are_accepted(validate_carrier, stage, dtype, carrier):
571+ validate_carrier(stage, dtype, carrier)
572+ 
573+ 
574+@pytest.mark.parametrize("stage", ["compile", "launch"])
575+@pytest.mark.parametrize("dtype,carrier", [
576+ (DataType.INT4, torch.float32),
577+ (DataType.UINT4, torch.int8),
578+ (DataType.FP4, getattr(torch, "uint32", None)),
579+ (DataType.FP4E2M1, getattr(torch, "uint32", None)),
580+ (DataType.FP4E1M2, getattr(torch, "uint32", None)),
581+ (DataType.FP4E1M2, getattr(torch, "float4_e2m1fn_x2", None)),
582+ (DataType.HF4, torch.int8),
583+ (DataType.FP8E4M3FN, getattr(torch, "float8_e5m2", None)),
584+ (DataType.FP8E5M2, getattr(torch, "float8_e4m3fn", None)),
585+ (DataType.UINT16, torch.float32),
586+ (DataType.UINT16, torch.int32),
587+ (DataType.INT16, torch.int32),
588+ (DataType.UINT32, torch.float32),
589+ (DataType.UINT32, torch.int16),
590+ (DataType.UINT32, torch.uint8),
591+ (DataType.FP64, torch.float32),
592+ (DataType.INDEX, torch.int32),
593+])
594+def test_wrong_tensor_carriers_raise(validate_carrier, stage, dtype, carrier):
595+ with pytest.raises(TypeError, match="dtype mismatch"):
596+ validate_carrier(stage, dtype, carrier)
597+ 
598+ 
531@pytest.mark.parametrize(599@pytest.mark.parametrize(
532 "dtype, expected",600 "dtype, expected",
533 [601 [
@@ -0,0 +1,57 @@
1+#!/usr/bin/env python3
2+# coding: utf-8
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+"""Host storage shapes are normalized once; explicit static shapes are already logical."""
12+ 
13+from types import SimpleNamespace
14+ 
15+from pypto_pro._errors import InvalidShape, InvalidType
16+import pypto_pro.language as pl
17+from pypto_pro.runtime.shape_policy import KernelSignatureSpec
18+import pytest
19+ 
20+ 
21+def _signature(dtype, shape):
22+ def kernel(x: pl.Tensor[shape, dtype]):
23+ pass
24+ return KernelSignatureSpec.from_callable(kernel)
25+ 
26+ 
27+@pytest.mark.parametrize("dtype", [pl.DT_FP4E2M1, pl.DT_FP4E1M2])
28+@pytest.mark.parametrize("shape", [[1, 64], [pl.DYNAMIC, 64], [1, pl.STATIC], [1, ...]])
29+def test_runtime_shapes_count_logical_elements(dtype, shape):
30+ bound = _signature(dtype, shape).bind_runtime_args((SimpleNamespace(shape=(1, 32)),))
31+ assert bound.tensor("x").dimensions[-1].value == 64
32+ 
33+ 
34+def test_explicit_static_shapes_are_not_unpacked_twice():
35+ signature = _signature(pl.DT_FP4E2M1, [1, pl.STATIC])
36+ assert signature.bind_static_shapes({"x": [1, 64]}).static_signature == ((0, 1, 64),)
37+ 
38+ 
39+@pytest.mark.parametrize("shape,error", [((1, 0), InvalidShape), ((1, -1), InvalidShape),
40+ ((1, True), InvalidType), ((32,), InvalidType)])
41+def test_invalid_carrier_shapes_remain_rejected(shape, error):
42+ with pytest.raises(error):
43+ _signature(pl.DT_FP4E2M1, [1, 64]).bind_runtime_args((SimpleNamespace(shape=shape),))
44+ 
45+ 
46+def test_byte_wide_types_and_other_axes_remain_unchanged():
47+ for dtype in (pl.DT_FP32, pl.DT_INT8):
48+ bound = _signature(dtype, [7, 32]).bind_runtime_args((SimpleNamespace(shape=(7, 32)),))
49+ assert [d.value for d in bound.tensor("x").dimensions] == [7, 32]
50+ bound = _signature(pl.DT_FP4E2M1, [7, 64]).bind_runtime_args((SimpleNamespace(shape=(7, 32)),))
51+ assert [d.value for d in bound.tensor("x").dimensions] == [7, 64]
52+ 
53+ 
54+def test_dynamic_packed_dimensions_keep_dynamic_policy():
55+ bound = _signature(pl.DT_FP4E2M1, [pl.DYNAMIC, pl.DYNAMIC]).bind_runtime_args(
56+ (SimpleNamespace(shape=(7, 32)),))
57+ assert all(dim.dynamic_name for dim in bound.tensor("x").dimensions)
@@ -0,0 +1,47 @@
1+#!/usr/bin/env python3
2+# coding: utf-8
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+"""Observers see the returned Program after AUTO ID resolution and stay invocation-local."""
12+ 
13+from pypto_pro import ir
14+import pypto_pro.language as pl
15+import pytest
16+ 
17+ 
18+@pl.jit(arch="3510")
19+def grouped_copy(a: pl.Tensor[[1, 64], pl.DT_FP32], out: pl.Tensor[[1, 64], pl.DT_FP32]):
20+ tt = pl.TileType(shape=[1, 64], dtype=pl.DT_FP32, target_memory=pl.MemorySpace.Vec)
21+ group = pl.make_tile_group(type=tt, addrs=[0, 256], mutex_ids="auto", depth=2)
22+ with pl.section_vector():
23+ tile = group.next()
24+ pl.load(tile, a, [0, 0])
25+ pl.store(out, tile, [0, 0])
26+ 
27+ 
28+def test_observer_sees_final_program_once():
29+ calls = []
30+ definition = grouped_copy.to_kernel_def()
31+ program, matched = definition.parse_target_program(
32+ ir.SectionKind.Vector, observer=lambda p, parser: calls.append((p, parser)))
33+ assert matched and len(calls) == 1
34+ assert calls[0][0].same_as(program)
35+ assert calls[0][1].tile_group_meta and calls[0][1]._tile_mutex_meta
36+ assert "__pypto_auto_mutex_id_" not in ir.python_print(program)
37+ definition.parse_target_program(ir.SectionKind.Cube)
38+ assert len(calls) == 1
39+ 
40+ 
41+def test_observer_errors_propagate_and_do_not_persist():
42+ definition = grouped_copy.to_kernel_def()
43+ def fail(program, parser):
44+ raise RuntimeError("observer sentinel")
45+ with pytest.raises(RuntimeError, match="observer sentinel"):
46+ definition.parse_target_program(ir.SectionKind.Vector, observer=fail)
47+ assert definition.parse_target_program(ir.SectionKind.Vector)[1]
@@ -0,0 +1,67 @@
1+#!/usr/bin/env python3
2+# coding: utf-8
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+"""Diagnostic rendering separates source previews from messages and locations."""
12+ 
13+import warnings
14+ 
15+from pypto_pro import ir
16+from pypto_pro._errors import (
17+ CheckSkipped,
18+ InvalidType,
19+ _warn_located,
20+ warn_check_skipped,
21+)
22+import pytest
23+ 
24+MARKER = "ONLY_IN_SOURCE_PREVIEW"
25+ 
26+ 
27+def _error():
28+ return InvalidType(
29+ "actual diagnostic",
30+ span={"filename": "kernel.py", "line": 2, "column": 1},
31+ source_lines=["first", MARKER, "third"],
32+ hint="use a Tile", note="a diagnostic note",
33+ )
34+ 
35+ 
36+def test_render_without_source_excludes_source_preview():
37+ error = _error()
38+ assert MARKER in str(error)
39+ assert MARKER not in error.render(with_source=False)
40+ assert error.message == "actual diagnostic"
41+ 
42+ 
43+def test_render_without_source_preserves_diagnostic_fields():
44+ diagnostic = _error().render(with_source=False)
45+ for pattern in ("actual diagnostic", "hint: use a Tile", "note: a diagnostic note", "kernel.py:2:1"):
46+ assert pattern in diagnostic
47+ 
48+ 
49+@pytest.mark.parametrize("setting,count", [("", 0), ("other", 0), ("skipped-checks", 1), ("other, all", 1)])
50+def test_skipped_checks_are_opt_in_and_located(monkeypatch, setting, count):
51+ monkeypatch.setenv("PYPTOPRO_INFO", setting)
52+ with warnings.catch_warnings(record=True) as recorded:
53+ warnings.simplefilter("always")
54+ warn_check_skipped("dynamic offset", ir.Span("a_kernel.py", 17, 3))
55+ assert len(recorded) == count
56+ if recorded:
57+ assert recorded[0].category is CheckSkipped
58+ assert recorded[0].filename == "a_kernel.py"
59+ assert recorded[0].lineno == 17
60+ 
61+ 
62+def test_warning_locations_have_separate_registries():
63+ with warnings.catch_warnings(record=True) as recorded:
64+ warnings.simplefilter("default", CheckSkipped)
65+ for filename in ("first_kernel.py", "second_kernel.py", "first_kernel.py"):
66+ _warn_located("same diagnostic", CheckSkipped, ir.Span(filename, 23, 1))
67+ assert [item.filename for item in recorded] == ["first_kernel.py", "second_kernel.py"]
@@ -286,7 +286,7 @@ def test_a_check_raised_inside_a_shared_helper_names_the_helper():
286def test_a_cpp_check_keeps_its_own_first_line_code_and_location():286def test_a_cpp_check_keeps_its_own_first_line_code_and_location():
287 """A C++ check reaches Python whole: its file:line, its code, its DSL location.287 """A C++ check reaches Python whole: its file:line, its code, its DSL location.
288 288 
289- ``adds`` declares no signature, so the type gate leaves its operands alone and289+ ``row_sum`` declares no signature, so the type gate leaves its operands alone and
290 the C++ deduction is the one to refuse a Tensor where a Tile belongs.290 the C++ deduction is the one to refuse a Tensor where a Tile belongs.
291 """291 """
292 292 
@@ -295,7 +295,7 @@ def test_a_cpp_check_keeps_its_own_first_line_code_and_location():
295 tile_type = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)295 tile_type = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)
296 tile = pl.make_tile(tile_type, addr=0)296 tile = pl.make_tile(tile_type, addr=0)
297 pl.load(tile, x, [0, 0])297 pl.load(tile, x, [0, 0])
298- pl.adds(tile, x, 1)298+ pl.row_sum(tile, x, tile)
299 299 
300 rendered = _render(kernel)300 rendered = _render(kernel)
301 assert rendered.head, rendered.lines[0]301 assert rendered.head, rendered.lines[0]
@@ -503,18 +503,18 @@ def test_more_cpp_checks_carry_their_own_source_and_the_qualified_enum():
503 """Sampled across C++ files, since each macro call site renders its own head."""503 """Sampled across C++ files, since each macro call site renders its own head."""
504 504 
505 @pl.jit505 @pl.jit
506- def adds_from_a_tensor(x: pl.Tensor[[64, 64], pl.DT_FP16]):506+ def row_sum_from_a_tensor(x: pl.Tensor[[64, 64], pl.DT_FP16]):
507 tile_type = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)507 tile_type = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)
508 tile = pl.make_tile(tile_type, addr=0)508 tile = pl.make_tile(tile_type, addr=0)
509 pl.load(tile, x, [0, 0])509 pl.load(tile, x, [0, 0])
510- pl.adds(tile, x, 1)510+ pl.row_sum(tile, x, tile)
511 511 
512 @pl.jit512 @pl.jit
513- def adds_into_a_tensor(x: pl.Tensor[[64, 64], pl.DT_FP16]):513+ def row_sum_into_a_tensor(x: pl.Tensor[[64, 64], pl.DT_FP16]):
514 tile_type = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)514 tile_type = pl.TileType(shape=[64, 64], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)
515 tile = pl.make_tile(tile_type, addr=0)515 tile = pl.make_tile(tile_type, addr=0)
516 pl.load(tile, x, [0, 0])516 pl.load(tile, x, [0, 0])
517- pl.adds(x, tile, 1)517+ pl.row_sum(x, tile, tile)
518 518 
519 @pl.jit519 @pl.jit
520 def rank_one_tile(x: pl.Tensor[[64, 64], pl.DT_FP16]):520 def rank_one_tile(x: pl.Tensor[[64, 64], pl.DT_FP16]):
@@ -522,7 +522,7 @@ def test_more_cpp_checks_carry_their_own_source_and_the_qualified_enum():
522 pl.load(pl.make_tile(tile_type, addr=0), x, [0, 0])522 pl.load(pl.make_tile(tile_type, addr=0), x, [0, 0])
523 523 
524 seen = set()524 seen = set()
525- for kernel in (adds_from_a_tensor, adds_into_a_tensor, rank_one_tile):525+ for kernel in (row_sum_from_a_tensor, row_sum_into_a_tensor, rank_one_tile):
526 rendered = _render(kernel)526 rendered = _render(kernel)
527 origin = rendered.head["origin"]527 origin = rendered.head["origin"]
528 # A check may expand in a .cpp or in a shared op header (the528 # A check may expand in a .cpp or in a shared op header (the
@@ -1065,7 +1065,7 @@ def test_an_op_without_a_declaration_is_not_type_checked_either():
1065 def kernel(x: pl.Tensor[[64, 64], pl.DT_FP16]):1065 def kernel(x: pl.Tensor[[64, 64], pl.DT_FP16]):
1066 tile = pl.make_tile(_tile_type(), addr=0)1066 tile = pl.make_tile(_tile_type(), addr=0)
1067 pl.load(tile, x, [0, 0])1067 pl.load(tile, x, [0, 0])
1068- pl.adds(tile, x, 1)1068+ pl.row_sum(tile, x, tile)
1069 1069 
1070 rendered = _render(kernel)1070 rendered = _render(kernel)
1071 # the C++ deduction is the one that refuses it, and says so in its own words1071 # the C++ deduction is the one that refuses it, and says so in its own words
@@ -0,0 +1,230 @@
1+#!/usr/bin/env python3
2+# coding: utf-8
3+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
4+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
5+# CANN Open Software License Agreement Version 2.0 (the "License").
6+# Please refer to the License for details. You may not use this file except in compliance with the License.
7+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
8+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
9+# See LICENSE in the root of the software repository for the full text of the License.
10+# -----------------------------------------------------------------------------------------------------------
11+"""Resource diagnostics preserve locations and stop at the first invalid allocation."""
12+ 
13+from types import SimpleNamespace
14+import warnings
15+ 
16+from pypto_pro import ir
17+from pypto_pro._errors import (
18+ CapacityUnknown,
19+ InvalidOperation,
20+ InvalidShape,
21+ InvalidTile,
22+ InvalidType,
23+ MutexMismatch,
24+ NotSupported,
25+ OutOfRange,
26+ current_span,
27+)
28+from pypto_pro.ir.op import block_ops
29+import pypto_pro.language as pl
30+import pytest
31+ 
32+ 
33+@pytest.mark.parametrize("space", [
34+ ir.MemorySpace.Bias, ir.MemorySpace.Scaling, ir.MemorySpace.ScaleLeft, ir.MemorySpace.ScaleRight,
35+])
36+def test_parameter_buffer_capacity_is_checked(space):
37+ capacity = block_ops._get_memory_capacity(space)
38+ assert capacity > 0
39+ with pytest.raises(OutOfRange) as caught:
40+ block_ops._validate_tile_addr_capacity(capacity, 1, space, ir.Span("kernel.py", 7, 2))
41+ assert caught.value.span["line"] == 7
42+ 
43+ 
44+@pytest.mark.parametrize("space,count", [
45+ (ir.MemorySpace.Vec, 1), (ir.MemorySpace.ScaleLeft, 1),
46+ (ir.MemorySpace.ScaleRight, 1), (ir.MemorySpace.DDR, 0),
47+])
48+def test_unknown_capacity_warns_only_for_bounded_spaces(monkeypatch, space, count):
49+ from pypto_pro.runtime import platform
50+ monkeypatch.setattr(platform, "get_memory_limit", lambda arch, space: 0)
51+ with warnings.catch_warnings(record=True) as recorded:
52+ warnings.simplefilter("always")
53+ assert block_ops._get_memory_capacity(space, ir.Span("capacity_kernel.py", 13, 1)) == 0
54+ assert len(recorded) == count
55+ if recorded:
56+ assert recorded[0].category is CapacityUnknown
57+ assert recorded[0].filename == "capacity_kernel.py" and recorded[0].lineno == 13
58+ assert space.name in str(recorded[0].message)
59+ 
60+ 
61+@pytest.mark.parametrize("explicit", [False, True])
62+@pytest.mark.parametrize("addr,error", [(1, InvalidTile), (0x40000, OutOfRange)])
63+def test_make_tile_error_uses_explicit_or_current_span(explicit, addr, error):
64+ span = ir.Span("explicit_kernel.py", 73, 9) if explicit else None
65+ with current_span(ir.Span("active_kernel.py", 42, 4)):
66+ with pytest.raises(error) as caught:
67+ block_ops.make_tile_expr([16, 16], pl.DT_FP16, ir.MemorySpace.Vec, addr=addr, span=span)
68+ location = caught.value.span
69+ expected = ("explicit_kernel.py", 73, 9) if explicit else ("active_kernel.py", 42, 4)
70+ assert (location["filename"], location["line"], location["column"]) == expected
71+ 
72+ 
73+@pytest.mark.parametrize("explicit", [False, True])
74+@pytest.mark.parametrize("addr,space,shape,dtype,error", [
75+ (0, ir.MemorySpace.Vec, [32, 16], pl.DT_FP16, OutOfRange),
76+ (2, ir.MemorySpace.DDR, [16, 16], pl.DT_FP32, InvalidTile),
77+])
78+def test_reinterpret_error_uses_explicit_or_current_span(explicit, addr, space, shape, dtype, error):
79+ tile = block_ops.make_tile_expr([16, 16], pl.DT_FP16, space, addr=addr)
80+ span = ir.Span("explicit_kernel.py", 73, 9) if explicit else None
81+ with current_span(ir.Span("active_kernel.py", 42, 4)):
82+ with pytest.raises(error) as caught:
83+ block_ops._ir_reinterpret(tile, shape=shape, dtype=dtype, span=span)
84+ location = caught.value.span
85+ expected = ("explicit_kernel.py", 73, 9) if explicit else ("active_kernel.py", 42, 4)
86+ assert (location["filename"], location["line"], location["column"]) == expected
87+ 
88+ 
89+@pytest.mark.parametrize("explicit", [False, True])
90+@pytest.mark.parametrize("reinterpret", [False, True])
91+def test_builder_capacity_warning_uses_explicit_or_current_span(monkeypatch, explicit, reinterpret):
92+ from pypto_pro.runtime import platform
93+ tile = block_ops.make_tile_expr([16, 16], pl.DT_FP16, ir.MemorySpace.Vec, addr=0)
94+ monkeypatch.setattr(platform, "get_memory_limit", lambda arch, space: 0)
95+ span = ir.Span("explicit_kernel.py", 73, 9) if explicit else None
96+ with current_span(ir.Span("active_kernel.py", 42, 4)):
97+ with warnings.catch_warnings(record=True) as recorded:
98+ warnings.simplefilter("always", CapacityUnknown)
99+ if reinterpret:
100+ block_ops._ir_reinterpret(tile, shape=[8, 16], span=span)
101+ else:
102+ block_ops.make_tile_expr([16, 16], pl.DT_FP16, ir.MemorySpace.Vec, addr=0, span=span)
103+ assert len(recorded) == 1 and recorded[0].category is CapacityUnknown
104+ expected = ("explicit_kernel.py", 73) if explicit else ("active_kernel.py", 42)
105+ assert (recorded[0].filename, recorded[0].lineno) == expected
106+ 
107+ 
108+@pytest.mark.parametrize("acc_addr", [0x40000, -1])
109+@pytest.mark.parametrize("probe", [False, True])
110+def test_kernel_stops_at_first_over_capacity_declaration(acc_addr, probe):
111+ @pl.jit(arch="3510")
112+ def oversized(x: pl.Tensor[[1], pl.DT_FP32]):
113+ vec_type = pl.TileType(shape=[1, 64], dtype=pl.DT_FP32, target_memory=pl.MemorySpace.Vec)
114+ acc_type = pl.TileType(shape=[16, 16], dtype=pl.DT_FP32, target_memory=pl.MemorySpace.Acc)
115+ _vec = pl.make_tile(vec_type, addr=0x40000)
116+ _acc = pl.make_tile(acc_type, addr=acc_addr)
117+ kernel_def = oversized.to_kernel_def()
118+ with pytest.raises(OutOfRange) as caught:
119+ if probe:
120+ from pypto_pro.runtime.pipeline._analyzer import probe_kernel_facts
121+ probe_kernel_facts(kernel_def)
122+ else:
123+ kernel_def.parse_target_program(ir.SectionKind.Cube)
124+ assert "Vec capacity" in caught.value.message
125+ assert "Acc capacity" not in caught.value.message
126+ location = caught.value.span
127+ assert location["filename"] == __file__
128+ with open(__file__, encoding="utf-8") as source:
129+ assert "_vec = pl.make_tile(vec_type, addr=0x40000)" in source.readlines()[location["line"] - 1]
130+ 
131+ 
132+def test_builder_error_inherits_the_active_call_span():
133+ with current_span(ir.Span("direct_kernel.py", 19, 4)):
134+ with pytest.raises(InvalidType) as caught:
135+ block_ops._ir_store(SimpleNamespace(type=None), SimpleNamespace(type=None), [0, 0])
136+ assert caught.value.span["filename"] == "direct_kernel.py"
137+ assert caught.value.span["line"] == 19
138+ 
139+ 
140+def test_registered_builder_error_keeps_the_dsl_call_location():
141+ @pl.jit(arch="3510")
142+ def invalid_move(x: pl.Tensor[[1], pl.DT_FP16]):
143+ tile = pl.make_tile(pl.TileType(shape=[16, 16], dtype=pl.DT_FP16,
144+ target_memory=pl.MemorySpace.Vec), addr=0)
145+ with pl.section_vector():
146+ pl.move(tile, tile, phase=pl.STPhase.Final)
147+ with pytest.raises(NotSupported) as caught:
148+ invalid_move.to_kernel_def().parse_target_program(ir.SectionKind.Vector)
149+ assert "phase is only supported" in caught.value.message
150+ location = caught.value.span
151+ assert location["filename"] == __file__
152+ with open(__file__, encoding="utf-8") as source:
153+ assert "pl.move(tile, tile, phase=pl.STPhase.Final)" in source.readlines()[location["line"] - 1]
154+ 
155+ 
156+def test_nested_builder_error_keeps_the_inner_call_location():
157+ @pl.jit(arch="3510")
158+ def invalid_scalar(x: pl.Tensor[[1], pl.DT_FP16]):
159+ tile = pl.make_tile(pl.TileType(shape=[16, 16], dtype=pl.DT_FP16,
160+ target_memory=pl.MemorySpace.Vec), addr=0)
161+ packed = pl.make_tile(pl.TileType(shape=[16, 32], dtype=pl.DT_FP8E4M3FN,
162+ target_memory=pl.MemorySpace.Vec), addr=0x1000)
163+ with pl.section_vector():
164+ pl.muls(tile, tile,
165+ pl.getval(packed, 0))
166+ with pytest.raises(NotSupported) as caught:
167+ invalid_scalar.to_kernel_def().parse_target_program(ir.SectionKind.Vector)
168+ assert "getval does not support container dtype" in caught.value.message
169+ location = caught.value.span
170+ assert location["filename"] == __file__
171+ with open(__file__, encoding="utf-8") as source:
172+ assert "pl.getval(packed, 0)" in source.readlines()[location["line"] - 1]
173+ 
174+ 
175+@pytest.mark.parametrize("shape,error", [([0, 16], InvalidShape), ([17, 16], OutOfRange)])
176+def test_validshape_error_keeps_the_dsl_call_location(shape, error):
177+ @pl.jit(arch="3510")
178+ def invalid_validshape(x: pl.Tensor[[1], pl.DT_FP16]):
179+ tile_type = pl.TileType(shape=[16, 16], dtype=pl.DT_FP16, target_memory=pl.MemorySpace.Vec)
180+ tile = pl.make_tile(tile_type, addr=0)
181+ with pl.section_vector():
182+ pl.set_validshape(tile, shape=shape)
183+ with pytest.raises(error) as caught:
184+ invalid_validshape.to_kernel_def().parse_target_program(ir.SectionKind.Vector)
185+ location = caught.value.span
186+ assert location["filename"] == __file__
187+ with open(__file__, encoding="utf-8") as source:
188+ assert "pl.set_validshape(tile, shape=shape)" in source.readlines()[location["line"] - 1]
189+ 
190+ 
191+def _manual_alias_kernel(addrs, ids, auto_mutex=True, include_auto=False):
192+ @pl.jit(arch="3510", auto_mutex=auto_mutex)
193+ def kernel(x: pl.Tensor[[1], pl.DT_FP32]):
194+ tt = pl.TileType(shape=[1, 64], dtype=pl.DT_FP32, target_memory=pl.MemorySpace.Vec)
195+ group = pl.make_tile_group(type=tt, addrs=addrs, mutex_ids=ids, depth=2)
196+ alias = group
197+ if include_auto:
198+ _automatic = pl.make_tile_group(type=tt, addrs=0, mutex_ids="auto", depth=1)
199+ with pl.section_vector():
200+ pl.adds(alias[0], alias[0], 1.0)
201+ return kernel
202+ 
203+ 
204+@pytest.mark.parametrize("addrs,ids,auto_mutex,count", [
205+ ([0, 0], [1, 2], True, 1),
206+ ([0, 128], [1, 2], True, 1),
207+ ([0, 0], [1, 1], True, 0),
208+ ([0, 0], [[1, 2], [2, 3]], True, 0),
209+ ([0, 256], [1, 1], True, 0),
210+ ([0, 0], None, True, 0),
211+ ([0, 0], [1, 2], False, 0),
212+])
213+def test_manual_mutex_warning_preserves_alias_and_reuse_contracts(addrs, ids, auto_mutex, count):
214+ with warnings.catch_warnings(record=True) as recorded:
215+ warnings.simplefilter("always", MutexMismatch)
216+ _manual_alias_kernel(addrs, ids, auto_mutex).to_kernel_def().parse_target_program(ir.SectionKind.Vector)
217+ assert len(recorded) == count
218+ if recorded:
219+ assert recorded[0].category is MutexMismatch
220+ assert recorded[0].filename == __file__
221+ 
222+ 
223+def test_native_auto_alias_rejection_runs_before_manual_warning():
224+ with warnings.catch_warnings(record=True) as recorded:
225+ warnings.simplefilter("always", MutexMismatch)
226+ with pytest.raises(InvalidOperation) as caught:
227+ _manual_alias_kernel([0, 0], [1, 2], include_auto=True).to_kernel_def().parse_target_program(
228+ ir.SectionKind.Vector)
229+ assert "overlapping address ranges must use the same mutex ID" in caught.value.message
230+ assert not recorded