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
Merged fromddddwe/pyptocodex/pro-diagnosticsintocann/pyptomaster
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 | ||
| 7 | abs | 7 | abs |
| 8 | add | 8 | add |
| 9 | +adds | ||
| 9 | and_ | 10 | and_ |
| 10 | div | 11 | div |
| 11 | maximum | 12 | maximum |
| 12 | mul | 13 | mul |
| 14 | +muls | ||
| 13 | neg | 15 | neg |
| 14 | relu | 16 | relu |
| 15 | sub | 17 | sub |
| 18 | +subs | ||
| 16 | xor | 19 | xor |
| 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 | ||
| 9 | functional_debugging | 9 | functional_debugging |
| 10 | +frontend_diagnostics | ||
| 10 | sanitizer | 11 | sanitizer |
| 11 | performance_optimization | 12 | performance_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_view | 740 | // 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 | + | ||
| 235 | TEST_F(IRStructEqTypeTest, TestTupleAndOtherTypes) | 270 | TEST_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 | |||
| 62 | import os | 67 | import os |
| 63 | import re | 68 | import re |
| 64 | import sys | 69 | import sys |
| 70 | +import warnings | ||
| 65 | 71 | ||
| 66 | from pypto.error import PyptoError | 72 | from pypto.error import PyptoError |
| 67 | from pypto.pypto_impl import ir | 73 | from pypto.pypto_impl import ir |
| @@ -377,6 +383,44 @@ _PRIOR_CONTEXT_LINES = 2 | |||
| 377 | _FOLLOWING_CONTEXT_LINES = 2 | 383 | _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 | + | ||
| 380 | class PyptoProError(PyptoError): | 424 | class 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 | return | 503 | 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 | return | 507 | 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 parse | 513 | # 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+ | |||
| 46 | from pypto_pro.ir._utils import _is_int, _normalize_expr, _to_make_tuple | 46 | from pypto_pro.ir._utils import _is_int, _normalize_expr, _to_make_tuple |
| 47 | 47 | ||
| 48 | from ..._errors import ( | 48 | from ..._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 this | 66 | # 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) -> | |||
| 1093 | def _validate_validshape_bounds( | 1096 | def _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 | continue | 1586 | continue |
| 1586 | if access_size is not None: | 1587 | if access_size is not None: |
| 1587 | t_size = access_size | 1588 | t_size = access_size |
| @@ -3548,15 +3549,15 @@ def _validate_tile_addr_alignment( | |||
| 3548 | return # DDR / Scaling / Bias — no enforced alignment | 3549 | 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 logging | 3578 | + 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 smaller | 3588 | # 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). Tile | 3589 | # 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 | return | 3623 | return |
| 3622 | end_addr = addr + size | 3624 | 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 exc | 3942 | ) 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_id | 3947 | global mem_id |
| 3945 | mem_id += 1 | 3948 | mem_id += 1 |
| 3946 | kwargs["memref_addr"] = addr | 3949 | 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 | - | ||
| 4040 | def _hw_attr(hw, field: str) -> "int | None": | 4038 | def _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.type | 4103 | 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_shape | 4125 | # 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's | 4195 | # 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 | ||
| 5042 | def _to_const_int(val: Any, span: Span | None = None) -> Expr: | 5053 | def _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 | |||
| 430 | def struct(*args: Any, **kwargs: Any) -> Any: | 436 | def 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++ struct | 441 | + 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 literal | 442 | + 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 AST | 445 | # 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 allocation | 268 | 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 only | 276 | This op never emits a cross-core barrier (``pipe_barrier`` would only |
| 272 | drain the calling core's pipes). When the initialized GM data is | 277 | 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 subsequent | 278 | 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_decl | 430 | @_api_decl |
| 411 | def add(out: Tile, lhs: Tile, rhs: Union[Tile, int, float]) -> None: | 431 | def 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_decl | 1255 | @_api_decl |
| 1236 | def pto_assert(condition: bool, /, format_str: Optional[str] = None, *args, loc: bool = False) -> None: | 1256 | def 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 the | 170 | # 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_consts | 177 | self._tilingkey_consts = tilingkey_consts |
| 175 | self._datatype_consts = datatype_consts | 178 | 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_meta | 613 | 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 | ) |
| 51 | from ._tuple_type_registry import TupleTypeKind | 53 | from ._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 | @staticmethod | 99 | @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 += 1 | 601 | 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 | |||
| 26 | from pypto_pro.ir.op._op_registry import _OP_REGISTRY | 26 | from pypto_pro.ir.op._op_registry import _OP_REGISTRY |
| 27 | from pypto_pro.ir.op.block_ops import block_ir_op | 27 | from pypto_pro.ir.op.block_ops import block_ir_op |
| 28 | 28 | ||
| 29 | -from ..._errors import BadFd, InvalidArgument, InvalidOperation, InvalidShape, InvalidType, InvalidVal, NotSupported | 29 | +from ..._errors import ( |
| 30 | + BadFd, | ||
| 31 | + InvalidArgument, | ||
| 32 | + InvalidOperation, | ||
| 33 | + InvalidShape, | ||
| 34 | + InvalidType, | ||
| 35 | + InvalidVal, | ||
| 36 | + NotSupported, | ||
| 37 | + current_span, | ||
| 38 | +) | ||
| 30 | from .. import _api as _language_api | 39 | from .. import _api as _language_api |
| 31 | from .._vf_api import Vf | 40 | from .._vf_api import Vf |
| 32 | from . import _argument_checker | 41 | from . 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 to | 79 | +# 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 is | 83 | # 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_dtype | 100 | + key = str(pl_dtype) |
| 101 | + _PL_DTYPE_TO_TORCH[key] = (*_PL_DTYPE_TO_TORCH.get(key, ()), torch_dtype) | ||
| 102 | 102 | ||
| 103 | 103 | ||
| 104 | for _pl_dtype, _torch_name in ( | 104 | for _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: | |||
| 466 | def _pack_factor(dtype: DataType | None) -> int: | 486 | def _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 buffer | 489 | + 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 counts | 490 | + 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-byte | 491 | 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 is | 492 | 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 are | 493 | 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 | ||
| 484 | def _pl_dtype_to_torch(dtype: DataType): | 504 | def _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: | |||
| 650 | class _LaunchPlan: | 670 | class _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 are | 673 | + 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 variables | 674 | 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 resolved | 675 | 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 are | 681 | 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 no | 683 | where ``rank`` is -1 for parameters whose shape is not checked (PTR, or no |
| 664 | declared shape), ``dyn_dims`` lists the dimensions this parameter contributes | 684 | declared shape), ``dyn_dims`` lists the dimensions this parameter contributes |
| 665 | to the dynamic ABI tail, and ``pack`` is the sub-byte packing factor from | 685 | 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 | continue | 820 | continue |
| 801 | - _, _, name, expected_dtype, rank, consts, dyn_dims, pack = slot | 821 | + _, _, 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 dynamic | 836 | # 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_signature | 2337 | 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, | ||
| 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 None | 362 | ) from None |
| 363 | + parser.warn_manual_mutex_aliases() | ||
| 364 | + if observer is not None: | ||
| 365 | + observer(program, parser) | ||
| 356 | return program, parser.matched_target | 366 | 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] = shape | 418 | 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) | ||
好像没找到使用的地方 ![]() ![]() | |||
| 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 | + | ||
| 96 | template <typename T> | 109 | template <typename T> |
| 97 | std::vector<T> ConvertSequenceToVector(py::handle obj) | 110 | std::vector<T> ConvertSequenceToVector(py::handle obj) |
| 98 | { | 111 | { |
| @@ -1540,6 +1553,17 @@ void BindProgram(py::module_& ir) | |||
| 1540 | 1553 | ||
| 1541 | void BindHelpers(py::module_& ir) | 1554 | void 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 | ||
| 241 | def test_dtype_without_shape_rejected_in_kernel(): | 242 | def 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_BF16 | 317 | 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 | ||
| 353 | def test_complex_integer_event_id_expression_is_accepted(): | 354 | def 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 | |||
| 22 | from unittest.mock import MagicMock | 22 | from unittest.mock import MagicMock |
| 23 | 23 | ||
| 24 | from pypto_pro._errors import InvalidShape | 24 | from pypto_pro._errors import InvalidShape |
| 25 | +from pypto_pro.language import STATIC, Tensor | ||
| 25 | from pypto_pro.runtime.platform import NpuArch | 26 | from pypto_pro.runtime.platform import NpuArch |
| 26 | import pytest | 27 | import 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 uint8 | 526 | +# 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 those | 527 | +# fp4 per byte, and torch reports the innermost axis in those |
| 527 | # storage units. ParamSpec and the kernel ABI count elements, so the launch plan | 528 | # 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(): | |||
| 286 | def test_a_cpp_check_keeps_its_own_first_line_code_and_location(): | 286 | def 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 and | 289 | + ``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.jit | 505 | @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.jit | 512 | @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.jit | 519 | @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 (the | 528 | # 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 words | 1071 | # 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 | ||


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