Files
paddlepaddle--paddle/python/paddle/incubate/cc/compiler.py
T
2026-07-13 12:40:42 +08:00

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:])