BackendHandler:原则化的后端分发¶
跟踪 issue:#948
背景¶
早期版本的 PyPTO 在 pass 与 codegen 中直接根据 backend::BackendType 做分支:
这种写法导致每新增一种后端,都要去整个代码库里翻这些散落的 if,并且为每一处
都补一条 case。BackendHandler 把所有这类分支集中到一个虚接口里,使新增后端
变成一次自包含的局部修改。
设计¶
BackendHandler(include/pypto/backend/common/backend_handler.h)是一个抽象
接口,它把后端之间所有"行为差异"显式命名出来。每个 Backend 子类各自持有一个
BackendHandler 子类的单例,并通过新引入的纯虚函数 Backend::GetHandler()
对外暴露。
┌────────────────────────────┐
Pass / Codegen │ PassContext::Current() │
│ ->GetBackendHandler() │
└─────────────┬──────────────┘
│
▼
┌────────────────────────────┐
│ BackendConfig::GetBackend │
│ ->GetHandler() │
└─────────────┬──────────────┘
│
▼
┌──────────────────────────────────────┐
│ Backend910B / Backend950 / ... │
│ -> Ascend910BHandler::Instance() │
│ -> Ascend950Handler::Instance() │
└──────────────────────────────────────┘
PassContext 上的便捷访问点满足 pass-context-config 规则的要求:pass 通过
当前 PassContext 查询后端行为,而不是直接读全局状态。
接口¶
| 方法 | 用途 | Ascend910 | Ascend950 |
|---|---|---|---|
GetPtoTargetArch() |
module attributes {pto.target_arch = "..."} |
"a2a3" |
"a5" |
GetSyncResourceCatalog() |
该后端持有的不可变同步资源信息 | A2/A3 资源池 | A5 资源池 |
GetLaunchSpecCoreCountMethod() |
launch_spec 上设置逻辑 block 数量的运行时 API 名 |
"set_block_num" |
"set_block_num" |
GetDefaultSimPlatform() |
默认仿真平台名 | "a2a3sim" |
"a5sim" |
GetExtraPtoasFlags() |
ptoas 额外参数 | [] |
["--pto-arch", "a5"] |
HonorsHighPrecisionAlgorithm() |
PTO-ISA 是否会根据 high_precision=True 生成的 precisionType 属性选择不同算法;为 false 时两个 PTO 发射路径(Tile 算子回调与 Buffer IR)都会告警该请求被丢弃 |
false |
true |
RequiresGMPipeBuffer() |
ExpandMixedKernel 是否注入 GM 槽位缓冲 |
true |
false |
SupportsLocalPipeDepth() |
本地消费者深度是否可独立于跨核传输环深度设置 | true |
false |
RequiresSplitLoadTpopWorkaround() |
MemoryReuse 是否做 load + tpop_from_aic 原地复用危害规避 | true |
false |
RequiresLevel3TmpScratch() |
InitMemRef 是否物化 level-3 显式 tmp;codegen static-view 桥接 | true |
false |
RequiresVtoCFractalAdapt() |
AIV 端 V→C tpush 是否需要 fractal 适配 tile.move |
false |
true |
RequiresRuntimeSubblockBridge() |
拆分 AIV 包装器是否从 runtime 上下文取 subblock id | true |
false |
RequiresNoSplitDualAivDispatch() |
no_split 混合 kernel 是否仍需在两个 AIV lane 上同时下发 |
true |
false |
SplitsCubeToVectorTransportInHardware() |
切分的 Cube-to-Vector push 是否由硬件把传输的 extent 各分一半给两个 AIV lane(此时该 extent 即 lane 划分) | false |
true |
GetL0cMAlignment(dtype) |
L0C 切分、dbC 容量与分配共同使用的物理 M 行对齐 | INT32 为 32;其余为 16 | 16 |
BuildCrossCoreTransferView(dest, view) |
跨核传输边界处的 tile 视图 | Mat/Left/Right 转 NZ;Vec 保持原样 | Mat/Left/Right 转 NZ(a5 硬件要求边界为 fractal);Vec 保持原样 |
注册算子规则¶
tile.sel、tile.sels 和 tile.cast 通过 f_plan_scratch 注册临时空间规则。
Select 的规则位于 pto_ops_select.cpp,cast 的规则与其代码生成一起位于
pto_ops_cast.cpp。目标架构在注册时绑定,查询不读取或修改全局后端。
tile.sels 另通过 f_validate 注册原有的类型与 MemRef 重叠检查。
Pass 使用 PassContext::GetBackend() 获取后端并查询规则。该接口保留现有的全局
目标解析方式,不引入上下文独立的目标所有权。未配置后端时,独立 select 降低仍保留
原来的 A5 临时空间规格和类型处理行为。
Backend::PlanOpScratch 返回的 Required 携带以元素为单位、只读的
TileScratchSpec;NotRequired 明确表示无需临时空间;Unknown 表示缺少必要
信息;Unsupported 表示该输入不受支持。已注册但没有规则的算子返回 None,
未注册的算子报错。查询本身不生成 IR,也不分配内存。
Cast 查询接收 source_tile 中不可变的物理与有效形状、target_dtype 和实际的
saturating 模式。若另外提供 source_dtype,它必须与 tile 一致。缺少参数或
容量计算依赖的维度为动态值时返回 Unknown;维数不合法时返回 Unsupported。
调用方在原始算子位置给出诊断。A5、原生转换及饱和转换无需静态形状便可返回
NotRequired。
from pypto import DataType, backend, ir
target = backend.get_backend_instance(backend.BackendType.Ascend910)
source = ir.TileType([4, 128], DataType.FP32, tile_view=ir.TileView(valid_shape=[3, 96]))
plan = target.plan_op_scratch(
"tile.cast", source_tile=source, target_dtype=DataType.INT16, saturating=False
)
# Required:INT8 [1, 1152],部分有效形状的容量计算包含物理行跨度。
InitMemRef 与代码生成均查询同一条 cast 规则。PyPTO、DSA-RP 根据返回的规格生成
显式 tmp;PTOAS 继续拥有隐式临时空间的分配权。代码生成给依赖临时空间的转换补充
静态有效范围视图。调用方显式传入的 tmp 保持不变,编译器合成规格不会变成对其
形状或类型的新限制。临时空间的生命周期与复用仍由内存分配及原有别名元数据决定。
内存容量来源¶
BackendHandler::GetCapacityBytes(memory_space) 调用 handler 所属后端的
Backend::GetMemSize()。SoC 是容量的唯一来源;handler 不重复保存数值,也不乘以核数
或 die 数。查询返回一个匹配内存组件的字节容量,类型为 uint64_t;SoC 未建模该空间
时返回 0。若多个核都有该空间,返回第一个匹配组件的容量。未建模不表示硬件没有该内存。
L0A、L0B、L0C、Bias、L1 分别使用 MemorySpace::Left、Right、Acc、Bias、Mat。
同一个接口也能查询 Vec 和 scale 内存。Python 接口为
handler.get_capacity_bytes(memory_space)。两种语言原有的五个独立容量方法均由该统一查询
替代。即使尚未配置全局目标,或者当前配置了另一种后端,显式查询该后端的 handler 仍能
返回正确容量。对齐规则、最小 tile 维度和成本模型参数仍与容量分别维护。
新增后端流程¶
- 在
src/backend/<arch>/backend_<arch>.cpp中实现Backend<Arch>子类, 并把源文件加入CMakeLists.txt。 - 在
src/backend/<arch>/backend_<arch>_handler.cpp中实现Backend<Arch>Handler子类。以所属后端引用初始化基类,该后端的生命周期必须长于 handler。内置 handler 使用后端单例;后端构造期间不能反过来请求 handler,避免单例 初始化重入。 - 让
Backend<Arch>::GetHandler()返回你的 handler 单例。 - 在
src/backend/common/backend.cpp中给GetBackendInstance和BackendTypeToString各加一行,并在BackendType枚举中添加新值。 同时将新后端加入src/backend/common/sync_resources.cpp中FindResourceCatalog使用的后端列表,使目标字符串查询能够访问新 handler 的同步资源目录。
任何 pass / codegen 文件都不需要修改。可以用
tests/ut/backend/test_backend_handler.py 配合 tests/ut/ir/transforms/ 与
tests/ut/codegen/ 里的回归测试做验证。
Python 访问方式¶
handler 也通过 Python 绑定暴露:
from pypto.pypto_core import backend as _backend_core
# 全局后端类型已配置时:
handler = _backend_core.get_handler()
# 调用方已知道目标后端时:
handler = _backend_core.get_backend_instance(BackendType.Ascend950).get_handler()
handler.get_pto_target_arch() # "a2a3" 或 "a5"
handler.requires_runtime_subblock_bridge() # bool
handler.supports_local_pipe_depth() # A2/A3 为 True,A5 为 False
handler.get_extra_ptoas_flags() # list[str]
运行时相关的 Python 模块(pypto.runtime.runner、pypto.ir.compiled_program、
pypto.backend.pto_backend)现在通过这些访问器来获取后端行为,不再直接判断
BackendType,与 C++ 改造保持一致。
Select 算子规则¶
tile.sel 与 tile.sels 在 BackendOpInfo::scratch_plan_func 注册 scratch
规划函数;TSELS 还在 validate_func 注册原有 dtype 与 MemRef 别名检查。
PTOCodegen 在输出 Tile PTO 前调用验证函数。两后端共享 pto_ops_select.cpp,
具体规则在注册阶段绑定。原有 GetTselScratchSpec、GetTselsScratchSpec、
SupportsTselDataType、SupportsTselsDataType 四个 handler 虚接口已移除。
Pass 经 PassContext::GetBackend() 查询算子规则。该入口保留现有全局目标解析语义,
不引入 context 独立持有目标的机制。未配置后端的独立 select lowering 仍使用历史
A5 scratch 几何,并保留原有不按后端限制 dtype 的行为。
from pypto import DataType, backend
target = backend.get_backend_instance(backend.BackendType.Ascend910)
plan = target.plan_op_scratch("tile.sels", DataType.FP16, source_cols=128)
# Required: FP16 [1, 128], one physical source row.
Required 携带以元素为单位的只读 TileScratchSpec;NotRequired 明确表示无需
scratch;Unknown 表示缺少查询所需信息;Unsupported 表示候选不支持。
已注册但没有规划函数的算子返回 None,未注册算子报错。查询不读取全局 target、
不创建 IR,也不分配内存。
合成规格与显式操作数保持区分:调用者提供的 scratch 原样传递,包括 A5 接受的其他 dtype 占位空间。本次迁移保留既有编译器检查,不增加 scratch 精确几何校验,不改变 PTOAS 校验。生命周期和复用决策仍由现有内存分配与 IR workspace/alias 元数据处理。