常见问题#

kernel函数出参未写回导致计算不生效#

问题现象描述#

当前PyPTO框架用pypto.frontend.jit装饰的kernel函数,不支持有返回值,输出需要通过参数的形式传入并使用[:]等进行写回操作,如果直接使用等号赋值,无法将数据写入输出Tensor中。

示例代码:

@pypto.frontend.jit
def add_kernel(x, y):
    pypto.set_vec_tile_shapes(4, 4)
    y = x + 1 # 此处会创建新的Tensor y

torch.npu.set_device(0)
x = torch.ones(4, 4, dtype=torch.float32)
y = torch.empty(4, 4, dtype=torch.float32)
add_kernel(pypto.from_torch(x), pypto.from_torch(y))
print(y) # 输出torch.empty创建的未经初始化的随机值

输出数据:

tensor([[2.0703e-19, 7.1833e+22, 1.8502e+28, 6.8608e+22],
        [4.8011e+30, 1.2123e+25, 4.7418e+30, 1.8465e+25],
        [1.2122e+25, 4.6114e+24, 1.7836e+31, 1.7591e+22],
        [1.1306e+24, 4.2245e-39, 6.8664e-44, 0.0000e+00]])

原因分析#

在add_kernel函数内部执行y =  x + 1时,这里的y是函数的局部变量(相当于创建了一个新的变量y),它会覆盖传入参数y的引用。也就是说,这行代码只是让函数内的y指向了x + 1的新Tensor,并不会修改外部传入的Tensor y的内容。

解决措施#

通过全切片操作符[:],将计算结果写入函数参数y的原有内存空间

示例代码:

@pypto.frontend.jit
def add_kernel(x, y):
    pypto.set_vec_tile_shapes(4, 4)
    y[:] = x + 1 # 将x+1的结果写入函数参数y的原有内存空间

torch.npu.set_device(0)
x = torch.ones(4, 4, dtype=torch.float32)
y = torch.empty(4, 4, dtype=torch.float32)
add_kernel(pypto.from_torch(x), pypto.from_torch(y))
print(y) # 输出x + 1的结果

输出数据:

tensor([[2., 2., 2., 2.],
        [2., 2., 2., 2.],
        [2., 2., 2., 2.],
        [2., 2., 2., 2.]])

其中y[:] = x + 1也可以替换为y.move(x + 1)或者y.assemble(x + 1, [0, 0])

未设置执行算子的设备id#

问题现象描述#

在昇腾AI处理器上执行算子时,出现失败,报错信息如下。

2025-12-17 14:31:32.491 E | fail get device id, check if set device id
2025-12-17 14:31:32.492 E | RuntimeAgent::AllocDevAddr failed for size 20448
2025-12-17 14:31:32.493 E | RuntimeAgent::AllocDevAddr failed for size 20448
2025-12-17 14:31:32.493 E | aclmdlRICaptureGetInfo failed, return[100000]

可能原因#

用户定义的算子未使用@jit进行装饰,且未使用torch_npu接口显式设置当前算子执行的Device ID。

处理步骤#

在算子执行前设置Device ID,例如:

def test_onboard():
    device_id = int(os.environ.get('TILE_FWK_DEVICE_ID', 0)) #从环境变量获取期望执行的device id
    torch.npu.set_device(device_id) #显式设置device id
    ....

CANN包不兼容#

问题现象描述#

算子上板执行时出现如下报错字样:

ErrorTracking callback in, task_id = 0, stream_id = 3.
[ERROR] Exception Type: exception invalid error
taskid: 0, streamid: 3, tid: 6495, deviceid: 0, retcode: 507018
kernelName = (null)
ErrorTracking callback in, task_id = 1, stream_id = 3.
[ERROR] Exception Type: exception invalid error
taskid: 1, streamid: 3, tid: 6495, deviceid: 0, retcode: 507018
kernelName = (null)

且device日志中出现类似如下接口为空报错:

~/ascend/log/debug/device-0/device-6495_20251222194004973.log
[ERROR] CCECPU(5670,aicpu_scheduler):2025-12-22-19:40:01.899.541 [ae_kernel_lib_aicpu_kfc.cpp:105][CallKernelApi][tid:5680][AICPU_PROCESSER] Get KFC DynTileFwkKernelServerInit api success, but func is nullptr: (null)
[ERROR] CCECPU(5670,aicpu_scheduler):2025-12-22-19:40:01.902.745 [ae_kernel_lib_aicpu_kfc.cpp:105][CallKernelApi][tid:5681][AICPU_PROCESSER] Get KFC DynTileFwkKernelServer api success, but func is nullptr: (null)

原因分析#

PyPTO驱动包支持25.2.0以上版本,CANN包支持8.5.0以上版本。

解决措施#

可以查看驱动包安装目录下的version信息,如:

/usr/local/Ascend/driver/version.info
    Version=25.3.rc1
    ascendhal_version=7.35.23
    aicpu_version=1.0
    tdt_version=1.0
    log_version=1.0
    prof_version=2.0
    dvppkernels_version=1.1
    tsfw_version=1.0
    Innerversion=V100R001C23SPC002B212
    compatible_version=[V100R001C19],[V100R001C20],[V100R001C21],[V100R001C22],[V100R001C23]
    compatible_version_fw=[7.0.0,8.9.9]
    package_version=25.3.rc1

同样可以查看CANN包安装目录下opp包内的version信息,如:

/usr/local/Ascend/ascend-toolkit/latest/opp/version.info
    Version=8.5.0.2.220
    version_dir=8.5.0
    timestamp=20251117_000024591
    required_package_amct_acl_version="8.5"

按上述方法查看驱动包和CANN包是否满足版本要求,如不满足请升级对应版本。

TileShape与Tensor维度不匹配#

问题现象描述#

算子执行时出现如下报错:

2025-12-18 10:33:06.107 E | [ExpandFunction][Function][ERROR]: FUnction[TENSOR_b_loop_Unroll1_PATH0_hiddenfunc0] ExpandFunction failed: Tile shape size 1 is not matched the output shape size 2.
2025-12-18 10:33:06.107 E | Run pass [ExpandFunction] failed.
2025-12-18 10:33:06.107 E | Run pass <ExpandFunction> failed

可能原因#

某个操作的TileShape设置的维度过小,小于该操作的输出Tensor的Shape维度,导致出现错误。

处理步骤#

根据报错提示定位到相应的循环,如下所说,问题代码出现在b_loop循环中。

FUnction[TENSOR_b_loop_Unroll1_PATH0_hiddenfunc0]

找到对应的循环后,根据日志提示的错误维度以及代码逻辑,确定代码中TileShape的维度为1,而输出Shape的维度为2,将TileShape重新设置为2维即可。

view未传入valid_shape导致精度问题#

问题现象描述#

部分场景使用view时未传入valid_shape导致精度问题

问题原因#

当view接口的输入tensor没有一个正确的validshape时,框架无法正确推导出输出的validshape。

处理步骤#

当怀疑view部分的validshape推导有问题时,首先给view传入一个validshape,观察输出结果是否符合预期。

一个典型的需要传入validshape的场景:

当输入的validShape依赖别的tensor标识,必须传入dynValidShape。如下所示场景,q0的validshape curSeq来自于另外一个tensor,无法通过推导得到

# 输入 input [B, S, H]
# 输入 act_seqs [B]
# 输出 out [B, S, H]
# 计算过程  AddS
# 代码如下:
for b_idx in pypto.loop(B, name="b_loop", idx_name="b"):
    cur_seq = act_seqs[b_idx]
    a0 = pypto.view(input, [1, S, H], [b_idx, 0, 0], valid_shape=[1, cur_seq, H])
    a1 = a0 + 1.0
    out[b_idx:, :, :] = a1

set_xxx_tile_shapes最后一维没有32字节对齐校验报错#

问题现象描述#

with pypto.function("TENSOR_SUM_FP32", [x], [res]):
        for _ in pypto.loop(1, name="LOOP_L0", idx_name="a_idx"):
            pypto.set_vec_tile_shapes(4, 8)
            res.move(x.sum())

通过pypto.set_xxx_tile_shapes设置TileShape大小最后一维需要32字节对齐,否则会校验报错。

C++ exception with description "ASSERTION FAILED: vecTile[lastDim] % alignNum == 0
Sum op: the tileShape of last axis need to 32Byte align!, func Sum, file reduction.cpp, line 374
libtile_fwk_interface.so(npu::tile_fwk::Sum(npu::tile_fwk::Tensor const&, int, bool)+0x620) [0xffff9ff2e090]

问题原因#

硬件指令限制处理的数据需要32字节对齐。

处理步骤#

通过pypto.set_xxx_tile_shapes设置TileShape大小需要将最后一维大小设置成32字节对齐的数,即TileShape[-1] * sizeof(dtype) % 32 == 0。

算子编译报堆栈溢出错误#

问题现象描述#

算子编译时出现类似如下报错字样:

error: stack frame size (*****) exceeds limit (32768) in function '*****'

在打屏日志中呈现样例如下:

error: stack frame size (47928) exceeds limit (32768) in function 'TENSOR_nLoop_Unroll1_PATH0_6_0_4503599627370496'
error: stack frame size (47928) exceeds limit (32768) in function 'TENSOR_nLoop_Unroll1_PATH0_6_0_4503599627370496'
2 errors generated.
terminate called after throwing an instance of 'npu::tile_fwk::Error'
  what():  ASSERTION FAILED: ret == 0
CompileCCE failed. errCode = 256, cce file: output/output_20251111_175724_806073/kernel_aicore/TENSOR_nLoop_Unroll1_PATH0_6_17699850674043372772_0_aic.cpp

原因分析#

由于硬件限制,昇腾AI处理器核内的标量处理单元栈空间最大为32K。因此,在算子编译时,底层的毕昇编译器会对算子核内函数的栈使用情况进行分析和校验。如果函数实现较为复杂,例如变量较多或变量生命周期较长等因素,均可能影响分析结果。如果最终分析结果超出32K限制,毕昇编译器将拦截并报告该错误。

在PTO编程模型下,算子核内函数实现复杂的主要原因主要如下:

  • 子图规模较大,导致变量过多。

  • 子图计算逻辑复杂,导致变量的生命周期较长。

解决措施#

针对上述可能的原因,可以采取以下措施进行处理:

  • 调整Tensor的Tiling切分策略,采用更大的Tile块进行切分。通过增加单个Tile块的计算数据量,在总数据量保持不变的情况下,可以减少子图所需的计算步骤,从而有效减小子图的规模。相关设置接口为:pypto.set_vec_tile_shapespypto.set_cube_tile_shapes

  • 对于矩阵乘(MATMUL)计算场景,建议用户对K轴进行多核切分。

  • 修改控制子图大小的选项”cycle_upper_bound”,将单个子图的最大规模限制在指定范围内。相关设置接口为:pypto.set_pass_options

循环中使用Python print函数打印#

问题现象描述#

@pypto.frontend.jit
def add_kernel_0(a, b, c):
    for i in pypto.loop(20):
        print("i = ", i)
        c[:] = a + b
>>>
i = 0

@pypto.frontend.jit
def add_kernel_1(a, b, c):
    for i in pypto.loop(20):
        print("i = ", i)
        if pypto.cond(i == 0):
            c[:] = a + b
        else:
            c[:] = a - b
>>>
i = 0
i = 1

可能原因#

用户算子描述的是构图过程,而非实际的执行逻辑。在构图阶段,Loop执行仅用于遍历所有执行路径。示例代码add_kernel_0中仅有一条执行路径,因此Loop仅执行一次。add_kernel_1中存在if/else两条路径,因此Loop执行两次。

处理步骤#

N/A

Loop 原理及其描述#

在编译阶段会将loop编译为控制流,将loop body转换为计算流,分别对应kernel_aicpu/kernel_aicore. kernel_aicpu 负责控制流的执行,创建kernel_aicore任务,包括使用到的内存分配以及执行参数准备, 同时分析输入输出的依赖将kernel_aicore任务合并成一个更大的调度单元,然后提交给调度单元进行调度

原型介绍#

def loop(start, end, step=1, name=None, idx_name=None,
         unroll_list = [1], submit_before_loop=False):

def loop_roll(start, end, step=1, name=None, idx_name=None,
         unroll_list = [1], submit_before_loop=False):

描述#

  1. start, end, step 分别可以表示循环的起始值、结束值和步长,类型可以为 SymbolicScalar 或者 int, 和Python中的range语法基本保持一致

  2. name 表示循环的名称,默认值为loop_{id}, 对实际使用运行效果无影响,仅用于调试和生成代码中注释信息

  3. idx_name 表示循环索引的名称,默认值为loop_idx_{id}, 对于嵌套累的Loop使用相同的idx会产生覆盖行为, 目前会在前端进行检查报错

  4. unroll_list 主要用于循环展开,产生更大的loop body, 降低调度开销。对于unroll_list=2, loop大概会产生如下代码

    new_start = start
    for k in unroll_list:
        left = (stop - start) % k
        for idx in loop(new_start, stop - left, k):
            for i in range(k):
                body(idx) # 需要用户一次处理step=1的步长
        new_start = stop - left
    

    loop_unroll 大概会产生如下代码

    new_start = start
    for k in unroll_list:
        left = (stop - start) % k
        for idx in loop(new_start, stop - left, k):
            body(idx, k) # 需要用户一次处理k的步长
        new_start = stop - left
    

    原则上如果可以一次处理多个i, 使用loop_unroll会更高效; 如果一次只能处理1个i, 则需要使用loop

  5. submit_before_loop 表示是否在循环开始前提交任务,默认值为False。如果设置为True,则循环前的任务会先提交到调度队列中,等待后续任务完成后再开始执行, 过多的设置submit_before_loop会增加调度开销, 建议仅在必要时设置为True

  6. unroll_list 对 pypto.cond 影响, 通常一个循环中有一个 pypto.cond, 会产生两个分支,当unroll次数为4次时,会产生 2 ** 4 16个路径分支,通常上每个分支都需要单独编译, 因此会大量增加编译时间和编译出的代码量. 为了支持关键算子FA的编译优化,提供了两个特殊的函数 pypto.is_loop_begin()pypto.is_loop_end() 用于优化条件分支

  7. 考虑到对外层loop进行loop_unroll不能提升loop body的大小, 当前仅支持最内侧循环进行unroll

  8. 为了写代码方便,可能有时看到前端算子并没有直接表达loop,是如何产生loop和loop body的呢. 实际是在构图阶段前端会隐式的在function开始的位置插入一个loop, 循环次数为1. 循环直到下一个循环开始前结束,举例如下

    @pypto.frontend.jit
    def foo(a, b, c):
        c[:] = a + b
    # 等价于
    @pypto.frontend.jit
    def foo(a, b, c):
        for i in pypto.loop(1):
            c[:] = a + b
    
    @pypto.frontend.jit
    def foo(a, b, c):
        t = a + 1
        for i in pypto.loop(1):
            c[:] = t + b
    # 等价于
    @pypto.frontend.jit
    def foo(a, b, c):
        for i in pypto.loop(1):
            t = a + 1
        for i in pypto.loop(1):
            c[:] = t + b
    

    框架当前不会自动进行loop(1)合并,因此在实际使用中,建议用户手动合并loop(1),以提高效率