Merged
feat(dsl): compile class-organized kernels #1292
feat(dsl): compile class-organized kernels #1292
Merged
yuantaoAdd on Sep 17
yuantao
yuantaoCollaborators
Sep 17

描述

让 tla.compile 接受「以类实例组织的 kernel」:编译期配置、kernel 与编译入口封装在同一个类里,实例交给 tla.compile 时,实例作为编译期接收者参与编排,self.attr 折叠为常量。

class VectorAdd:
    def __init__(self, vector_ele: int) -> None:
        self.vector_ele = vector_ele

    @tla.jit
    def __call__(self, lhs, rhs, output):
        self.kernel(lhs, rhs, output)

    @tla.kernel
    def kernel(self, lhs, rhs, output):
        ...  # self.vector_ele 作为编译期常量

opts = {"options": "--npu-arch 3510"}

# 1. 编译实例:靠 @tla.jit __call__ 的一行纯转发指出要编译的 kernel
a = tla.compile(VectorAdd(128), lhs, rhs, output, **opts)

# 2. 编译从实例读到的绑定方法:实例随属性读取一并作为 receiver
b = tla.compile(VectorAdd(128).kernel, lhs, rhs, output, **opts)

# 3. 换一份配置:编译期常量不同,得到新的产物
d = tla.compile(VectorAdd(64), lhs, rhs, output, **opts)

a(lhs, rhs, output, block_num=1)

写法 1 与 2 编译出同一份产物,cache_key 相同;启动签名都不含接收者:

写法 编译的 kernel 启动签名
tla.compile(实例, ...) @tla.jit __call__ 一行转发指向的 kernel 方法 lhs, rhs, output
tla.compile(实例.kernel, ...) 同一个方法,实例随属性读取作为 receiver lhs, rhs, output

换一份编译期配置(写法 3)得到新的 cache_key:以新增样例为例,n_ele=64 为 74035fa27c9e26e8,n_ele=384 为 fb2175bc67b0adb9。

改动涉及的文件:

文件 内容
catlass/base_dsl/compiler.py 实例入口解析 _resolve_kernel_target、@tla.jit __call__ 纯转发校验 _check_jit_call_is_pure_forwarding
catlass/dsl.py TlaJitFunction 增加 receiver 字段与 __get__,从实例取值时绑定实例
catlass/base_dsl/runtime/jit_arg_adapters.py receiver_argument_name:按位置与名字判定首位接收者
catlass/base_dsl/jit_executor.py ExecutionArgs.filter_runtime_signature 复用同一套静态参数规则,让启动签名与编译计划对齐
catlass/execution_lowering.py 接收者不占运行时 ABI 槽位,self.attr 折叠为常量

编译入口的确定规则:

类里的写法 编译的 kernel
传实例,类声明了 @tla.jit __call__ __call__ 一行转发指向的方法,方法名不限
传从实例读到的 kernel 方法 实例.kernel 该方法本身
传实例,类没有 @tla.jit __call__ 报错,提示补一个 @tla.jit __call__;不做「按方法名 kernel」等回落
传的不是 @tla.kernel 函数、也不是声明了它的类实例 报错,提示入口类型

接收者规则:

  • 首位参数名为 self 时视为编译期接收者:不占运行时 ABI 槽位,self.attr 在 kernel 编排时折叠为常量
  • receiver_argument_name 以「无宿主」(owning_func=None)调用判定规则,而 cls 需要 classmethod 宿主才成立,因此这条路径实际只把 self 认作接收者;首位 cls 不会成为接收者
  • 判定只看位置与参数名,与函数是否定义在类里无关
  • 接收者的取值若是运行时取值(设备 tensor、运行时数值),会直接报错并提示改名,避免它被静默丢出运行时 ABI
  • 绑定方法(实例.kernel)已经带上 receiver,不要再把实例作为实参传一次:tla.compile 会在拼接前检测到重复传入并明确报错

对既有行为的影响:首位 self 不再进入运行时 ABI,仓库内两处按旧语义书写的代码随之调整。

文件 调整
examples/end_to_end/structured_arguments/static_tensor_interfaces.py copy_from_self 改名 copy_from_tensor,首位参数改名 input_tensor
tests/test_execution_args.py self 不再出现在启动签名;cls 在没有 classmethod 归属时仍是运行时参数

检视意见处理(CheaterAbec)

检视意见 处理
纯转发校验函数名易误解:它只管 @tla.jit __call__ 是否一行纯转发,与 Host JIT 全路径无关 已改名为 _check_jit_call_is_pure_forwarding
「编译入口的确定规则」写了「方法名 kernel」「类内唯一 @tla.kernel 方法」两条回落,代码里没有 描述已按代码更正:无 @tla.jit __call__ 直接报错,不做回落
receiver_argument_name 的 cls 分支是死路径(判定时固定传 owning_func=None) 描述已更正:该路径只认 self
实例.kernel 已带 receiver,再显式传实例会重复传入 采纳「检测并明确诊断」:tla.compile 在拼接前对重复 receiver 抛错,并加用例 test_bound_method_rejects_a_duplicated_receiver
描述称 @tla.kernel 作用于 staticmethod / classmethod 会在装饰期报错 本 PR 未包含该诊断(dsl.py 只新增 receiver 字段与 __get__,测试也无对应用例),描述已删除该条

关联的Issue

https://gitcode.com/cann/catlass/issues/559

原因

issue #559 要求 CATLASS DSL 支持以类实例组织 Kernel,让分块大小、stage 数、dtype 这类编译期配置与 kernel 一起封装,而不是散在 host 侧的调用点。

改动前存在两个障碍:

现象 原因
传实例报 TlaUnsupportedAbiError: tla.compile expects a @tla.kernel function, or an instance of a class that declares one. tla.compile 只接受 @tla.kernel 的返回值
实例形态的 kernel 在 lowering 时实参个数不匹配 方法首位 self 被算作运行时参数

有意不支持的写法:host 侧没有对应的 pass,因此 @tla.jit __call__ 只能是到 kernel 的纯转发——单条语句、按序传全部参数、不用关键字、不计算实参。其他写法都会报错,这是本次变更的边界;实例若没有 @tla.jit __call__,也直接报错,不做「按方法名或唯一性猜入口」的回落。

测试

单元测试(python/tla_dsl,Python 3.11 venv,python -m pytest tests/):

  • 新增 tests/frontend/test_kernel_class_organization.py,27 个用例:实例与绑定方法两种写法产物等价(cache_key 相同、启动签名一致)、入口选择(转发指向的方法名不限)、转发形态校验与报错文案、无 @tla.jit __call__ 的实例被拒、接收者不占 ABI 槽位、self.attr(含嵌套普通对象属性)折叠为常量、首位 self 在类外也同样是接收者、运行时取值误当接收者被拒、按配置区分的 cache key
  • 定向:tests/frontend/test_kernel_class_organization.py + tests/test_execution_args.py 共 83 passed
  • 全量:2471 passed, 7 skipped;另有 13 failed 全部落在 tests/test_tla_lower_to_hivm.py,是本机 TlaCompile 二进制未随分支重建(缺 tla-vector-region-simd pass)所致,与本改动无关
  • lint:按 CI 口径 ruff check --select E4,E7,E9,F --line-length 120 与 format 通过;新增测试文件里 _BareNameCallKernel.__call__ 的裸名调用是刻意用例,已按仓内既有做法在 pyproject.toml 补 per-file-ignores

端到端(Ascend 950,--npu-arch 3510):类版本 vector-add 编译并启动,结果逐元素精确一致,n_ele=64 与 n_ele=384 均 passed=True,cache_key 分别为 74035fa27c9e26e8、fb2175bc67b0adb9。内存 / 性能 / 确定性检测不涉及。

新增样例:examples/end_to_end/kernel_in_class/vector_add_with_static_length.py,类版本 vector-add,编译期配置、kernel 与编译入口封装在同一个类里。

文档更新

文件 内容
docs/zh/kernel_development/core_concepts/compile_and_launch.md 新增「以类实例组织 Kernel」章节,含编译入口写法表与约束
docs/zh/api/host_api_reference.md tla.compile 参数与约束同步
docs/en/api/host_api_reference.md 同步生成结果与源码行号
examples/end_to_end/kernel_in_class/README.md 类版本示例说明

类型标签


Description

Let tla.compile accept a kernel organized as a class instance: compile-time configuration, the kernel and its compile entry live in one class, and the instance joins compilation as a compile-time receiver, so self.attr folds into constants.

class VectorAdd:
    def __init__(self, vector_ele: int) -> None:
        self.vector_ele = vector_ele

    @tla.jit
    def __call__(self, lhs, rhs, output):
        self.kernel(lhs, rhs, output)

    @tla.kernel
    def kernel(self, lhs, rhs, output):
        ...  # self.vector_ele as a compile-time constant

opts = {"options": "--npu-arch 3510"}

# 1. the instance: the one-line @tla.jit __call__ forward names the kernel
a = tla.compile(VectorAdd(128), lhs, rhs, output, **opts)

# 2. the bound method read off an instance: the instance rides along as the receiver
b = tla.compile(VectorAdd(128).kernel, lhs, rhs, output, **opts)

# 3. another configuration: different constants, a different binary
d = tla.compile(VectorAdd(64), lhs, rhs, output, **opts)

a(lhs, rhs, output, block_num=1)

Forms 1 and 2 compile to the same artifact and share a cache_key; neither launch signature carries the receiver:

Form Kernel compiled Launch signature
tla.compile(instance, ...) the kernel method the one-line @tla.jit __call__ forwards to lhs, rhs, output
tla.compile(instance.kernel, ...) the same method, the instance bound along as the receiver lhs, rhs, output

Another compile-time configuration (form 3) gets a new cache_key: in the new sample, n_ele=64 gives 74035fa27c9e26e8 and n_ele=384 gives fb2175bc67b0adb9.

Files touched:

File What
catlass/base_dsl/compiler.py Instance entry resolution _resolve_kernel_target, the @tla.jit __call__ pure-forwarding check _check_jit_call_is_pure_forwarding
catlass/dsl.py TlaJitFunction gains a receiver field and __get__, binding the instance when read off it
catlass/base_dsl/runtime/jit_arg_adapters.py receiver_argument_name: a leading receiver is decided by position and name
catlass/base_dsl/jit_executor.py ExecutionArgs.filter_runtime_signature reuses the same static-parameter rule so the launch signature lines up with the compiled plan
catlass/execution_lowering.py The receiver occupies no runtime ABI slot; self.attr folds into constants

How the entry point is picked:

Shape of the class Kernel compiled
An instance, and the class declares @tla.jit __call__ The method the one-line forward points to; the name is free
A kernel method read off an instance (instance.kernel) That method itself
An instance whose class has no @tla.jit __call__ Rejected, asking for a @tla.jit __call__; no "method named kernel" fallback
Neither a @tla.kernel function nor an instance of a class declaring one Rejected, naming the expected entry types

Receiver rule:

  • A leading parameter named self is a compile-time receiver: it takes no runtime ABI slot, and self.attr folds into the kernel as a constant while the body is staged
  • receiver_argument_name calls the rule with no owner (owning_func=None), and cls only qualifies under a classmethod owner, so on this path only self is ever taken as the receiver; a leading cls is not
  • The rule keys off position and name only, not on whether the function belongs to a class
  • A runtime receiver value (a device tensor or a runtime number) is rejected with a rename hint instead of leaving the runtime ABI unannounced
  • A bound method (instance.kernel) already carries the receiver; passing the instance again is a duplicate that tla.compile rejects with a clear message

Impact on existing behaviour: a leading self no longer reaches the runtime ABI, so two places in the tree that relied on the old reading are adjusted.

File Adjustment
examples/end_to_end/structured_arguments/static_tensor_interfaces.py copy_from_self becomes copy_from_tensor, its leading parameter becomes input_tensor
tests/test_execution_args.py self leaves the launch signature; cls stays a runtime parameter when no classmethod owns it

Review follow-up (CheaterAbec)

Comment Disposition
The pure-forwarding helper's name misleads: it only decides whether @tla.jit __call__ is a one-line forward, unrelated to the full Host JIT path Renamed to _check_jit_call_is_pure_forwarding
The entry-selection table claimed two fallbacks ("method named kernel", "the class's only @tla.kernel method") that the code does not have The description now matches the code: no @tla.jit __call__ is rejected outright, with no fallback
receiver_argument_name's cls branch is dead (the rule is called with owning_func=None) The description now states that this path only takes self
instance.kernel already carries the receiver, so passing the instance again would duplicate it Took the diagnosis route: tla.compile rejects the duplicate receiver before prepending, with a dedicated test
The description said @tla.kernel over staticmethod / classmethod is diagnosed at decoration time This PR does not carry that diagnostic (dsl.py only adds the receiver field and __get__, and no test covers it), so the claim is removed

https://gitcode.com/cann/catlass/issues/559

Reason

Issue #559 asks CATLASS DSL to support organizing a kernel as a class instance, so that compile-time configuration such as tile size, stage count and dtype travels with the kernel instead of being spread across host call sites.

Two things blocked it:

Symptom Cause
TlaUnsupportedAbiError: tla.compile expects a @tla.kernel function, or an instance of a class that declares one. when passing an instance tla.compile only accepted the object returned by @tla.kernel
Arity mismatch during lowering for an instance-shaped kernel The method's leading self was counted as a runtime parameter

Deliberately unsupported: there is no host pass for it, so a @tla.jit __call__ has to be a plain forward to the kernel — one statement, every parameter in order, no keywords, no computed arguments. Any other shape is rejected; that is the boundary of this change. An instance whose class has no @tla.jit __call__ is rejected too, with no "guess the entry from the method name or uniqueness" fallback.

Test

Unit tests (python/tla_dsl, Python 3.11 venv, python -m pytest tests/):

  • New tests/frontend/test_kernel_class_organization.py, 27 cases: the instance and bound-method forms agreeing (same cache_key, same launch signature), entry selection (the forwarded method's name is free), forward-shape validation and its messages, an instance without @tla.jit __call__ being rejected, the receiver taking no ABI slot, self.attr (including a nested plain attribute object) folding to constants, a leading self being a receiver outside a class too, a runtime receiver being rejected, per-configuration cache keys
  • Targeted: tests/frontend/test_kernel_class_organization.py + tests/test_execution_args.py, 83 passed
  • Full suite: 2471 passed, 7 skipped; the 13 failed all sit in tests/test_tla_lower_to_hivm.py and come from a locally built TlaCompile that predates the branch (missing the tla-vector-region-simd pass), unrelated to this change
  • Lint: ruff check --select E4,E7,E9,F --line-length 120 and format pass under the CI setup; the deliberate bare-name call in the new test file is covered by a per-file-ignores entry, following the repo's existing practice

End-to-end (Ascend 950, --npu-arch 3510): the class-organized vector add compiles and launches with element-wise exact results; n_ele=64 and n_ele=384 both report passed=True, with cache_key 74035fa27c9e26e8 and fb2175bc67b0adb9. Memory, performance and determinism checks are not applicable.

New sample: examples/end_to_end/kernel_in_class/vector_add_with_static_length.py, the class version of the vector add with compile-time configuration, kernel and compile entry in one class.

Documentation

File What
docs/zh/kernel_development/core_concepts/compile_and_launch.md New "以类实例组织 Kernel" section with the entry-form table and constraints
docs/zh/api/host_api_reference.md tla.compile arguments and constraints
docs/en/api/host_api_reference.md Regenerated output and source line numbers
examples/end_to_end/kernel_in_class/README.md Notes on the class version of the example

Type

likedislike
Pull Request has been successfully merged by CANN-robot.
(Thank you for yuantao's contribution.)
yuantaoyuantaoCollaborators
Sep 17 Created pull request, commit f325505e
yuantaoyuantaoCollaborators
Sep 17 Associated issue: [Requirement|需求建议]: CATLASS DSL 支持以类实例组织 Kernel
atomgit-bot
atomgit-bot
Sep 17 Comments:

变更摘要

本 PR 支持将 CATLASS DSL kernel 组织为类实例来编译:编译期配置(tile size、stage 数、dtype 等)作为实例属性与 kernel 及其编译入口放在一起,使 tla.compile 可以直接接收声明了 @tla.kernel 方法的类实例。kernel 方法的第一个 self 参数被识别为编译期 receiver,不占运行时 ABI 槽位,self.attr 读取在 body 编排(staging)时折叠为常量;tla.compile 按「@tla.jit __call__ 单行转发指向的方法 → 名为 kernel 的方法 → 类中唯一的 @tla.kernel 方法」的优先级选取入口,并同步调整启动签名以对齐编译计划。

主要改动

  • 类实例编译入口:在 compiler.py 中新增 _resolve_kernel_target、_instance_kernel_target、_pure_forward_target 等逻辑,CompileCallable 支持接收声明了 @tla.kernel 方法的类实例,将实例作为首个样本拼入 type_args,并对无 kernel 方法、入口歧义、转发参数数量不符等情况给出可操作的报错信息。
  • 编译期 receiver 识别与常量折叠:在 execution_lowering.py 中新增 _method_receiver_name(依据 __qualname__ 判定类成员,避免把普通函数中恰好名为 self 的参数误判为 receiver),LoweredTlaIR 新增 receiver_name 字段;receiver 被加入 constexpr_names 且不生成运行时 ABI 叶子,使 self.attr(含嵌套普通属性对象)在编排时折叠为常量。
  • 启动签名对齐:在 execution.py 中新增 _launch_signature,编译时从 kernel 签名中剔除 receiver 参数,保证启动参数与编译后的计划一致。
  • 装饰器顺序诊断:在 dsl.py 中新增 _reject_descriptor_wrapped_dsl_function,并由 core.py 在装饰期调用,当 @tla.kernel/@tla.jit 包裹 staticmethod/classmethod 描述符时立即提示正确的装饰器顺序。
  • 测试与文档:新增 tests/test_kernel_class_organization.py(16 个用例,覆盖入口选择、转发校验、receiver 无 ABI 槽位、self.attr 常量折叠、装饰器顺序诊断、按配置区分的缓存 key 等),并同步更新了相关测试桩与中英文 API 参考文档。
likedislike
Incorrect?
atomgit-bot
atomgit-bot
Sep 17 Comments:

代码审查

✅ 未发现问题

likedislike
Incorrect?
CANN-robotCANN-robotCollaborators
Sep 17 Added label: cann-cla/yes
252 messages are collapsed here。 See more
CANN-robotCANN-robotCollaborators
26 days ago Added label: approved
CheaterAbec
CheaterAbecCollaborators
26 days ago Comments:

/lgtm

likedislike
CANN-robotCANN-robotCollaborators
26 days ago Added label: lgtm
CANN-robotCANN-robotCollaborators
26 days ago Merged pull request
CANN-robot
CANN-robotCollaborators
26 days ago Comments:

Pull Request 已合并或已关闭。

If you want to solve this problem, you can click here to do it in the FAQs.

likedislike