293 lines
9.3 KiB
Python
293 lines
9.3 KiB
Python
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import os
|
|
from collections import OrderedDict
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
import paddle
|
|
from paddle.incubate.cc.tools import apy_to_axpr_json
|
|
from paddle.static import InputSpec
|
|
|
|
from . import typing as pct
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
__all__ = ['compile']
|
|
|
|
|
|
# Usage:
|
|
# import paddle.incubate.cc.typing as pct
|
|
# import paddle.incubate.cc as pcc
|
|
# import paddle.nn.functional as F
|
|
#
|
|
# N = pct.DimVar('N', min=2)
|
|
# K = pct.DimVar("K", min=2)
|
|
# M = pct.DimVar("M", 7168)
|
|
# DType = pct.DTypeVar("T", "bfloat16", "float32")
|
|
#
|
|
# def foo(
|
|
# x: pct.Tensor([N, K], DType),
|
|
# y: pct.Tensor([K, M], DType),
|
|
# b: pct.Tensor([M], DType)
|
|
# ):
|
|
# @pcc.force_register_fusion
|
|
# def activate(out):
|
|
# return F.relu(out + b)
|
|
# return activate(x @ y)
|
|
#
|
|
# fused_foo = pcc.compile(
|
|
# foo
|
|
# )
|
|
def compile(func, *args, **kwargs):
|
|
annotations = _get_input_annotations(func)
|
|
dtypes2func = {}
|
|
for input_specs in _get_input_spec_lists(annotations):
|
|
dtypes = tuple(input_spec.dtype for input_spec in input_specs)
|
|
dtypes2func[dtypes] = _compile(func, input_specs, *args, **kwargs)
|
|
return OverloadedFunc(FuncOverloadCtx(dtypes2func))
|
|
|
|
|
|
def _compile(
|
|
func,
|
|
input_specs,
|
|
train=False,
|
|
ap_path="",
|
|
ap_workspace_dir='/tmp/paddle/ap',
|
|
backend_device='cuda',
|
|
target_framework='paddle',
|
|
compile_engine='PCC',
|
|
):
|
|
assert ap_path is not None
|
|
assert not train, "only support inference now"
|
|
assert backend_device in ["cuda", "dcu", "custom_device"]
|
|
os.makedirs(ap_workspace_dir, exist_ok=True)
|
|
build_strategy = paddle.static.BuildStrategy()
|
|
assert compile_engine in ('CINN', 'PCC')
|
|
with _ap_envs(ap_path, ap_workspace_dir, backend_device):
|
|
static_fn = paddle.jit.to_static(
|
|
func,
|
|
input_spec=input_specs,
|
|
build_strategy=build_strategy,
|
|
full_graph=True,
|
|
backend=compile_engine,
|
|
)
|
|
if not train:
|
|
static_fn.eval()
|
|
else:
|
|
static_fn.train()
|
|
concrete_program, partial_program_layer = (
|
|
static_fn.get_concrete_program(
|
|
*input_specs, is_train=static_fn._is_train_mode()
|
|
)
|
|
)
|
|
partial_program_layer.training = static_fn._is_train_mode()
|
|
# Force to generate the program immediately.
|
|
if train:
|
|
_ = partial_program_layer.train_program.forward_program
|
|
else:
|
|
_ = partial_program_layer.infer_program.forward_program
|
|
return partial_program_layer
|
|
|
|
|
|
@dataclass
|
|
class FuncOverloadCtx:
|
|
dtypes2func: dict[list[paddle.dtype], Callable]
|
|
|
|
|
|
class OverloadedFunc:
|
|
def __init__(self, func_overload_ctx: FuncOverloadCtx):
|
|
self.func_overload_ctx = func_overload_ctx
|
|
|
|
def __call__(self, *args):
|
|
dtypes = tuple(tensor.dtype for tensor in args)
|
|
func = self.func_overload_ctx.dtypes2func.get(dtypes, None)
|
|
assert func is not None, self.mismatched_debug_info(dtypes)
|
|
return func(inputs=[*args])
|
|
|
|
def mismatched_debug_info(self, dtypes):
|
|
valid_signatures = "; ".join(
|
|
f"[{idx + 1}] {dtypes}"
|
|
for idx, pair in enumerate(
|
|
self.func_overload_ctx.dtypes2func.items()
|
|
)
|
|
for dtypes in [pair[0]]
|
|
)
|
|
return f"input signature {dtypes} mismatched, valid signatures are: {valid_signatures}"
|
|
|
|
|
|
@dataclass
|
|
class InputSpecMakeCtx:
|
|
name2dtype_num_candidates: dict[str, int]
|
|
name2dtype_candidate_idx: dict[str, int]
|
|
|
|
|
|
@contextmanager
|
|
def _ap_envs(ap_path, ap_workspace_dir, backend_device):
|
|
old_ap_workspace_dir = os.environ.get('AP_WORKSPACE_DIR')
|
|
new_ap_path, old_ap_path = _get_ap_path(ap_path, backend_device)
|
|
_convert_apy_to_axpr(new_ap_path)
|
|
os.environ['AP_PATH'] = new_ap_path
|
|
os.environ['AP_WORKSPACE_DIR'] = ap_workspace_dir
|
|
new_flags, old_flags = _get_ap_flags()
|
|
paddle.set_flags(new_flags)
|
|
old_prim_all = paddle.base.core._is_all_prim_enabled()
|
|
paddle.base.core._set_prim_all_enabled(True)
|
|
try:
|
|
yield
|
|
finally:
|
|
os.environ['AP_PATH'] = old_ap_path
|
|
if old_ap_workspace_dir is not None:
|
|
os.environ['AP_WORKSPACE_DIR'] = old_ap_workspace_dir
|
|
paddle.set_flags(old_flags)
|
|
paddle.base.core._set_prim_all_enabled(old_prim_all)
|
|
|
|
|
|
def _get_ap_path(ap_path, backend_device):
|
|
ap_sys_path = f"{os.path.dirname(paddle.__file__)}/apy/sys"
|
|
matmul_path = f"{os.path.dirname(paddle.__file__)}/apy/matmul_pass"
|
|
if backend_device in ["cuda", "dcu"]:
|
|
device_path = (
|
|
f"{os.path.dirname(paddle.__file__)}/apy/device/{backend_device}"
|
|
)
|
|
else:
|
|
device_path = ""
|
|
old_ap_path = os.environ.get('AP_PATH')
|
|
new_ap_path = f"{ap_sys_path}:{ap_path}:{device_path}:{matmul_path}:{old_ap_path if old_ap_path is not None else ''}"
|
|
if old_ap_path is None:
|
|
# Always add sys_path to AP_PATH, as it is required at runtime.
|
|
old_ap_path = ap_sys_path
|
|
return new_ap_path, old_ap_path
|
|
|
|
|
|
def _get_ap_flags():
|
|
old_flags = paddle.get_flags(
|
|
['FLAGS_enable_ap', 'FLAGS_prim_enable_dynamic']
|
|
)
|
|
new_flags = dict(old_flags)
|
|
new_flags['FLAGS_enable_ap'] = True
|
|
new_flags['FLAGS_prim_enable_dynamic'] = True
|
|
return new_flags, old_flags
|
|
|
|
|
|
def _convert_apy_to_axpr(ap_path):
|
|
all_ap_paths = {p for p in ap_path.split(":") if p and os.path.isdir(p)}
|
|
for path in all_ap_paths:
|
|
apy_to_axpr_json.PyToAxpr(path)(path)
|
|
|
|
|
|
def _get_input_annotations(func):
|
|
full_arg_spec = inspect.getfullargspec(func)
|
|
return [
|
|
pct_type
|
|
for arg_name in full_arg_spec.args
|
|
for pct_type in [full_arg_spec.annotations[arg_name]]
|
|
]
|
|
|
|
|
|
def _get_input_spec_lists(annotations):
|
|
ctx = _create_empty_input_spec_make_ctx(annotations)
|
|
assert len(ctx.name2dtype_num_candidates) > 0
|
|
dtype_var_names = [
|
|
pair[0] for pair in ctx.name2dtype_num_candidates.items()
|
|
]
|
|
dtype_num_candidates = [
|
|
pair[1] for pair in ctx.name2dtype_num_candidates.items()
|
|
]
|
|
dtype_candidate_idx_compositions = _cartesian_product(
|
|
[range(num_candidates) for num_candidates in dtype_num_candidates]
|
|
)
|
|
for idx_composition in dtype_candidate_idx_compositions:
|
|
for arg_idx, candidate_idx in enumerate(idx_composition):
|
|
ctx.name2dtype_candidate_idx[dtype_var_names[arg_idx]] = (
|
|
candidate_idx
|
|
)
|
|
yield _get_input_specs(annotations, ctx)
|
|
|
|
|
|
def _create_empty_input_spec_make_ctx(annotations):
|
|
ctx = InputSpecMakeCtx(OrderedDict(), OrderedDict())
|
|
_init_empty_input_spec_make_ctx(annotations, ctx)
|
|
return ctx
|
|
|
|
|
|
def _init_empty_input_spec_make_ctx(annotations, mut_ctx: InputSpecMakeCtx):
|
|
for pct_type in annotations:
|
|
_init_input_spec_make_ctx_name2dtype_num_candidates(pct_type, mut_ctx)
|
|
|
|
|
|
def _init_input_spec_make_ctx_name2dtype_num_candidates(
|
|
pct_type, mut_ctx: InputSpecMakeCtx
|
|
):
|
|
assert isinstance(pct_type.dtype, pct.DTypeVar), (
|
|
f"pct_type.dtype should be a DTypeVar, but {type(pct_type.dtype)} were given."
|
|
)
|
|
name = pct_type.dtype.name
|
|
if name in mut_ctx.name2dtype_num_candidates:
|
|
assert mut_ctx.name2dtype_num_candidates[name] == len(
|
|
pct_type.dtype.candidates
|
|
)
|
|
else:
|
|
mut_ctx.name2dtype_num_candidates[name] = len(pct_type.dtype.candidates)
|
|
|
|
|
|
def _get_input_specs(annotations, ctx: InputSpecMakeCtx):
|
|
return [_get_input_spec(pct_type, ctx) for pct_type in annotations]
|
|
|
|
|
|
def _get_input_spec(pct_type, ctx: InputSpecMakeCtx):
|
|
assert isinstance(pct_type, pct.Tensor)
|
|
return InputSpec(
|
|
shape=_get_input_spec_shape(pct_type, ctx),
|
|
dtype=_get_input_spec_dtype(pct_type, ctx),
|
|
)
|
|
|
|
|
|
def _get_input_spec_shape(pct_type, ctx: InputSpecMakeCtx):
|
|
return [_get_input_spec_shape_dim(dim_var) for dim_var in pct_type.shape]
|
|
|
|
|
|
def _get_input_spec_shape_dim(dim_var: pct.DimVar):
|
|
if isinstance(dim_var, int):
|
|
return dim_var
|
|
assert isinstance(dim_var, pct.DimVar)
|
|
if isinstance(dim_var.name_or_value, int):
|
|
return dim_var.name_or_value
|
|
return None
|
|
|
|
|
|
def _get_input_spec_dtype(pct_type, ctx: InputSpecMakeCtx):
|
|
assert isinstance(pct_type.dtype, pct.DTypeVar)
|
|
name = pct_type.dtype.name
|
|
candidate_idx = ctx.name2dtype_candidate_idx[name]
|
|
return pct_type.dtype.candidates[candidate_idx]
|
|
|
|
|
|
def _cartesian_product(lst_of_lst):
|
|
assert len(lst_of_lst) > 0
|
|
return _cartesian_product_impl([()], lst_of_lst)
|
|
|
|
|
|
def _cartesian_product_impl(collect_lst, lst_of_lst):
|
|
if len(lst_of_lst) == 0:
|
|
return collect_lst
|
|
collect_lst = [(*x, y) for x in collect_lst for y in lst_of_lst[0]]
|
|
return _cartesian_product_impl(collect_lst, lst_of_lst[1:])
|