Files
paddlepaddle--paddle/paddle/fluid/operators/generator/parse_utils.py
T
2026-07-13 12:40:42 +08:00

715 lines
25 KiB
Python

# Copyright (c) 2022 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 re
from copy import copy
from typing import Any
from tests_utils import is_attr, is_input, is_output, is_vec
from type_mapping import opmaker_attr_types_map
def to_named_dict(items: list[dict], is_op=False) -> dict[str, dict]:
named_dict = {}
if is_op:
for item in items:
if "name" not in item:
raise KeyError(f"name not in {item}")
item["name"] = (
item["name"] if item["name"][-1] != '_' else item["name"][:-1]
)
if "forward" in item:
item["forward"]["name"] = (
item["forward"]["name"]
if item["forward"]["name"][-1] != '_'
else item["forward"]["name"][:-1]
)
name = item["name"]
named_dict[name] = item
else:
for item in items:
if "name" not in item:
raise KeyError(f"name not in {item}")
name = item["name"]
named_dict[name] = item
return named_dict
def parse_arg(op_name: str, s: str) -> dict[str, str]:
"""parse an argument in following formats:
1. typename name
2. typename name = default_value
"""
typename, rest = (item.strip() for item in s.split(" ", 1))
assert len(typename) > 0, (
f"The arg typename should not be empty. Please check the args of {op_name} in yaml."
)
assert rest.count("=") <= 1, (
f"There is more than 1 = in an arg in {op_name}"
)
if rest.count("=") == 1:
name, default_value = (item.strip() for item in rest.split("=", 1))
assert len(name) > 0, (
f"The arg name should not be empty. Please check the args of {op_name} in yaml."
)
assert len(default_value) > 0, (
f"The default value should not be empty. Please check the args of {op_name} in yaml."
)
return {
"typename": typename,
"name": name,
"default_value": default_value,
}
else:
name = rest.strip()
assert len(name) > 0, (
f"The arg name should not be empty. Please check the args of {op_name} in yaml."
)
return {"typename": typename, "name": name}
def parse_input_and_attr(
op_name: str, arguments: str
) -> tuple[list, list, dict, dict]:
args_str = arguments.strip()
assert args_str.startswith('(') and args_str.endswith(')'), (
f"Args declaration should start with '(' and end with ')', "
f"please check the args of {op_name} in yaml."
)
args_str = args_str[1:-1]
args = parse_plain_list(args_str)
inputs = []
attrs = []
met_attr_with_default_value = False
for arg in args:
item = parse_arg(op_name, arg)
typename = item["typename"]
name = item["name"]
if is_input(typename):
assert len(attrs) == 0, (
f"The input Tensor should appear before attributes. "
f"please check the position of {op_name}:input({name}) "
f"in yaml."
)
inputs.append(item)
elif is_attr(typename):
if met_attr_with_default_value:
assert "default_value" in item, (
f"{op_name}: Arguments with default value should not precede those without default value"
)
elif "default_value" in item:
met_attr_with_default_value = True
if typename.startswith('Scalar') or typename == 'IntArray':
item['data_type'] = opmaker_attr_types_map[typename]
attrs.append(item)
else:
raise KeyError(f"{op_name}: Invalid argument type {typename}.")
return inputs, attrs
def parse_output(op_name: str, s: str) -> dict[str, str]:
"""parse an output, typename or typename(name)."""
match = re.search(
r"(?P<out_type>[a-zA-Z0-9_[\]]+)\s*(?P<name>\([a-zA-Z0-9_@]+\))?\s*(?P<expr>\{[^\}]+\})?",
s,
)
typename = match.group("out_type")
name = match.group("name")
size_expr = match.group("expr")
name = name[1:-1] if name is not None else 'out'
size_expr = size_expr[1:-1] if size_expr is not None else None
assert is_output(typename), (
f"Invalid output type: {typename} in op : {op_name}."
f"Supported types are Tensor and Tensor[]"
)
if size_expr is not None:
assert is_vec(typename), (
f"Invalid output size: output {name} in op : {op_name} is "
f"not a vector but has size expr"
)
return {"typename": typename, "name": name, "size": size_expr}
else:
return {"typename": typename, "name": name}
def parse_outputs(op_name: str, outputs: str) -> list[dict]:
if outputs is None:
return []
outputs = parse_plain_list(outputs, sep=",")
output_items = []
for output in outputs:
output_items.append(parse_output(op_name, output))
return output_items
def parse_infer_meta(infer_meta: dict[str, Any]) -> dict[str, Any]:
infer_meta = copy(infer_meta) # to prevent mutating the input
if "param" not in infer_meta:
infer_meta["param"] = None
return infer_meta
def parse_candidates(s: str) -> dict[str, Any]:
"parse candidates joined by either '>'(ordered) or ','(unordered)"
delimiter = ">" if ">" in s else ","
ordered = delimiter == ">"
candidates = parse_plain_list(s, delimiter)
candidates = list(filter(None, candidates))
return {"ordered": ordered, "candidates": candidates}
def parse_plain_list(s: str, sep=",") -> list[str]:
if sep == ",":
pattern = re.compile(r',(?![^{]*\})') # support "int[] a={1,2}"
items = re.split(pattern, s.strip())
items = [x.strip() for x in items]
return items
else:
return [item.strip() for item in s.strip().split(sep)]
def parse_kernel(op_name: str, kernel_config: dict[str, Any]) -> dict[str, Any]:
# kernel :
# func : [], Kernel functions (example: scale, scale_sr)
# param : [], Input params of kernel
# backend : str, the names of param to choose the kernel backend, default is None
# layout : str, the names of param to choose the kernel layout, default is None
# data_type : str, the names of param to choose the kernel data_type, default is None
# dispatch : {}, the key is kernel_func, the value is type of inputs and outputs for kernel (example: {kernel_name : (['dense','sparse_coo']#input,['sparse_coo']#output)})
kernel = {
'func': [], # up to 2 function names
'param': None,
'backend': None,
'layout': None,
'data_type': None,
'dispatch': {},
'force_backend': None,
}
if 'param' in kernel_config:
kernel['param'] = kernel_config['param']
if 'force_backend' in kernel_config:
kernel['force_backend'] = kernel_config["force_backend"]
if 'backend' in kernel_config:
kernel['backend'] = parse_candidates(kernel_config["backend"])
if 'layout' in kernel_config:
kernel['layout'] = parse_candidates(kernel_config["layout"])
if 'data_type' in kernel_config:
data_type_item = parse_candidates(kernel_config["data_type"])
params_num = len(data_type_item['candidates'])
data_type_item['to_complex_flag'] = [False] * params_num
for i in range(params_num):
complex_match_result = re.match(
r"complex\((?P<param_name>\w+)\)",
data_type_item['candidates'][i],
)
if complex_match_result:
data_type_item['candidates'][i] = complex_match_result.group(
'param_name'
)
data_type_item['to_complex_flag'][i] = True
kernel['data_type'] = data_type_item
kernel_funcs = re.compile(r'([a-zA-Z0-9_]+)\s*({[^}]+})?').findall(
kernel_config['func']
)
def parse_kernel_in_out_type(in_out_str):
if len(in_out_str) == 0:
return None
tmp_in_out_list = in_out_str[1:-1].split('->')
inputs = [item.strip() for item in tmp_in_out_list[0].split(',')]
outputs = [item.strip() for item in tmp_in_out_list[1].split(',')]
# check the tensor type
for item in inputs:
assert item in [
'dense',
'selected_rows',
'sparse_coo',
'sparse_csr',
], (
f"{op_name} : Invalid input tensor type ('{item}'), here we only support 'dense', 'selected_rows', 'sparse_coo' and 'sparse_csr'."
)
for item in outputs:
assert item in [
'dense',
'selected_rows',
'sparse_coo',
'sparse_csr',
], (
f"{op_name} : Invalid output tensor type ('{item}'), here we only support 'dense', 'selected_rows', 'sparse_coo' and 'sparse_csr'."
)
return (inputs, outputs)
for func_item in kernel_funcs:
kernel['func'].append(func_item[0])
kernel['dispatch'][func_item[0]] = parse_kernel_in_out_type(
func_item[1]
)
return kernel
def delete_bracket(name: str):
if name[0] == "(":
name = name.lstrip("(")
if name[-1] == ")":
name = name.rstrip(")")
return name
def parse_inplace(op_name: str, inplace_cfg: str) -> dict[str, str]:
inplace_map = {}
inplace_cfg = inplace_cfg.lstrip("(").rstrip(")")
pairs = parse_plain_list(inplace_cfg)
for pair in pairs:
in_name, out_name = parse_plain_list(pair, sep="->")
in_name = delete_bracket(in_name)
out_name = delete_bracket(out_name)
inplace_map[out_name] = in_name
return inplace_map
def parse_invoke(op_name: str, invoke_config: str) -> dict[str, Any]:
invoke_config = invoke_config.strip()
func, rest = invoke_config.split("(", 1)
func = func.strip()
args = rest[:-1].strip() # deal the last ')'
invocation = {"func": func, "args": args}
return invocation
def extract_type_and_name(records: list[dict]) -> list[dict]:
"""extract type and name from forward call, it is simpler than forward op ."""
extracted = [
{"name": item["name"], "typename": item["typename"]} for item in records
]
return extracted
def parse_forward(op_name: str, forward_config: str) -> dict[str, Any]:
# op_name (const Tensor& input, ... , int attr, ...) -> Tensor(out)
result = re.search(
r"(?P<op>[a-z][a-z0-9_]+)\s*(?P<args>\([^\)]+\))\s*->\s*(?P<outputs>.+)",
forward_config,
)
op = result.group("op")
outputs = parse_outputs(op_name, result.group("outputs"))
outputs = extract_type_and_name(outputs)
inputs, attrs = parse_input_and_attr(op_name, result.group("args"))
inputs = extract_type_and_name(inputs)
attrs = extract_type_and_name(attrs)
forward_cfg = {
"name": op,
"inputs": inputs,
"attrs": attrs,
"outputs": outputs,
}
return forward_cfg
def parse_composite(
op_name: str,
composite_config: str,
) -> dict[str, Any]:
# composite_config: func(args1, args2,.....)
result = re.search(
r"(?P<func_name>[a-z][a-z0-9_]+)\s*\((?P<func_args>[^\)]+)\)",
composite_config,
)
func_name = result.group("func_name")
func_args = result.group("func_args")
composite_dict = {}
composite_dict["func_name"] = func_name
composite_dict["func_args"] = func_args
return composite_dict
def check_op_config(op_entry, op_name):
base_key_set = (
'op',
'backward_op',
'forward',
'args',
'output',
'infer_meta',
'kernel',
'backward',
'invoke',
'inplace',
'view',
'optional',
'intermediate',
'no_need_buffer',
'data_transform',
'composite',
'support_dygraph_mode',
'support_tensor',
'traits',
'interfaces',
'python_api',
)
infer_meta_key_set = (
'func',
'param',
'spmd_rule',
'local_shape',
'global_shape',
)
kernel_key_set = (
'func',
'param',
'data_type',
'layout',
'backend',
'force_backend',
'python_api',
'dispatch',
)
for key in op_entry.keys():
assert key in base_key_set, (
f"Op ({op_name}) : invalid key ({key}) in Yaml."
)
if 'infer_meta' in op_entry:
for infer_meta_key in op_entry['infer_meta'].keys():
assert infer_meta_key in infer_meta_key_set, (
f"Op ({op_name}) : invalid key (infer_meta.{infer_meta_key}) in Yaml."
)
if 'kernel' in op_entry:
for kernel_key in op_entry['kernel'].keys():
assert kernel_key in kernel_key_set, (
f"Op ({op_name}) : invalid key (kernel.{kernel_key}) in Yaml."
)
def parse_op_entry(op_entry: dict[str, Any], name_field="op"):
op_name = op_entry[name_field]
inputs, attrs = parse_input_and_attr(op_name, op_entry["args"])
outputs = parse_outputs(op_name, op_entry["output"])
if "composite" in op_entry:
composite_dict = parse_composite(op_name, op_entry["composite"])
check_op_config(op_entry, op_name)
# validate default value of DataType and DataLayout
for attr in attrs:
if "default_value" in attr:
typename = attr["typename"]
default_value = attr["default_value"]
if typename == "DataType":
assert "DataType" in default_value, (
f"invalid DataType default value in {op_name}"
)
# remove namespace
default_value = default_value[default_value.find("DataType") :]
attr["default_value"] = default_value
elif typename == "DataLayout":
assert "DataLayout" in default_value, (
f"invalid DataLayout default value in {op_name}"
)
default_value = default_value[
default_value.find("DataLayout") :
]
attr["default_value"] = default_value
input_names = [item["name"] for item in inputs]
attr_names = [item["name"] for item in attrs]
output_names = [item["name"] for item in outputs]
# add optional tag for every input
for input in inputs:
input["optional"] = False
for output in outputs:
output["optional"] = False
if "optional" in op_entry:
optional_args = parse_plain_list(op_entry["optional"])
for name in optional_args:
assert name in input_names or name in output_names, (
f"{op_name} has an optional tensor: '{name}' which is not in input or output."
)
for input in inputs:
if input["name"] in optional_args:
input["optional"] = True
for output in outputs:
if output["name"] in optional_args:
output["optional"] = True
# add intermediate tag for every output
for output in outputs:
output["intermediate"] = False
if "intermediate" in op_entry:
intermediate_outs = parse_plain_list(op_entry["intermediate"])
for name in intermediate_outs:
assert name in output_names, (
f"{op_name} has an intermediate output: '{name}' which is not an output."
)
for output in outputs:
if output["name"] in intermediate_outs:
output["intermediate"] = True
# add no_need_buffer for every input
for input in inputs:
input["no_need_buffer"] = False
if "no_need_buffer" in op_entry:
no_buffer_args = parse_plain_list(op_entry["no_need_buffer"])
for name in no_buffer_args:
assert name in input_names, (
f"{op_name} has an no buffer input: '{name}' which is not an input."
)
for input in inputs:
if input["name"] in no_buffer_args:
input["no_need_buffer"] = True
else:
no_buffer_args = None
# add data_transform tag for every input.
# the format is {data_transform : {skip_transform : [x, z], support_trans_dtype : y}}
for input in inputs:
input["data_transform"] = {}
if "data_transform" in op_entry:
skip_trans_args = []
support_trans_args = []
data_trans = op_entry["data_transform"]
if "skip_transform" in data_trans:
skip_trans_args = parse_plain_list(data_trans["skip_transform"])
for name in skip_trans_args:
assert name in input_names, (
f"{op_name} has an skip_transform input: '{name}' which is not an input."
)
data_trans["skip_transform"] = skip_trans_args
if "support_trans_dtype" in data_trans:
support_trans_args = parse_plain_list(
data_trans["support_trans_dtype"]
)
for name in support_trans_args:
assert name in input_names, (
f"{op_name} has an support_trans_dtype input: '{name}' which is not an input."
)
data_trans["support_trans_dtype"] = support_trans_args
for input in inputs:
if input["name"] in skip_trans_args:
input["data_transform"]["skip_trans_args"] = True
else:
input["data_transform"]["skip_trans_args"] = False
if input["name"] in support_trans_args:
input["data_transform"]["support_trans_dtype"] = True
else:
input["data_transform"]["support_trans_dtype"] = False
else:
data_trans = None
if "support_tensor" in op_entry.keys():
support_tensor = op_entry["support_tensor"]
else:
support_tensor = []
if "traits" in op_entry.keys():
trait_list = parse_plain_list(op_entry["traits"])
else:
trait_list = []
if "interfaces" in op_entry.keys():
interface_list = parse_plain_list(op_entry["interfaces"])
else:
interface_list = []
op = {
"name": op_name,
"inputs": inputs,
"attrs": attrs,
"outputs": outputs,
"no_need_buffer": no_buffer_args,
"data_transform": data_trans,
"support_tensor": support_tensor,
"traits": trait_list,
"interfaces": interface_list,
}
# op should be is_base_op or is_invoke_op or is_only_composite_op
is_base_op = True
if "invoke" in op_entry:
is_base_op = False
if "composite" in op_entry and "kernel" not in op_entry:
is_base_op = False
if is_base_op:
# kernel
if "kernel" in op_entry:
kernel = parse_kernel(op_name, op_entry["kernel"])
if kernel["param"] is None:
kernel["param"] = input_names + attr_names
op.update({"kernel": kernel})
# infer meta
if "infer_meta" in op_entry:
infer_meta = parse_infer_meta(op_entry["infer_meta"])
if infer_meta["param"] is None:
infer_meta["param"] = copy(kernel["param"])
op.update({"infer_meta": infer_meta})
# else:
# assert(outputs == []), f"No infer_meta is given in {op_name}."
# inplace
if "inplace" in op_entry:
inplace_pairs = parse_inplace(op_name, op_entry["inplace"])
else:
inplace_pairs = None
# view
if "view" in op_entry:
view_pairs = parse_inplace(op_name, op_entry["view"])
else:
view_pairs = None
op.update(
{
"inplace": inplace_pairs,
"view": view_pairs,
}
)
# has invoke ?
if "invoke" in op_entry:
invoke_dict = parse_invoke(op_name, op_entry["invoke"])
op.update({"invoke": invoke_dict})
# has composite ?
if "composite" in op_entry:
op.update({"composite": composite_dict})
# backward
if "backward" in op_entry:
backward = op_entry["backward"]
else:
backward = None
op["backward"] = backward
# forward for backward_ops
is_backward_op = name_field == "backward_op"
if is_backward_op:
if "forward" in op_entry:
forward = parse_forward(op_name, op_entry["forward"])
# validate_fb
validate_backward_inputs(
op_name, forward["inputs"], forward["outputs"], inputs
)
validate_backward_attrs(op_name, forward["attrs"], attrs)
validate_backward_outputs(op_name, forward["inputs"], outputs)
else:
forward = None
op["forward"] = forward
# parse python_api
if "python_api" in op_entry:
op.update({"python_api": op_entry["python_api"]})
return op
def validate_backward_attrs(op, forward_attrs, backward_attrs):
if len(forward_attrs) >= len(backward_attrs):
return
num_exceptional_attrs = len(backward_attrs) - len(forward_attrs)
# this is a not-that-clean trick to allow backward op to has more attrs
# than the forward op , as long as they all have default value
for i in range(-num_exceptional_attrs, 0):
assert "default_value" in backward_attrs[i], (
f"{op} has exceptional attr without default value"
)
def validate_backward_inputs(
op, forward_inputs, forward_outputs, backward_inputs
):
forward_input_names = [item["name"] for item in forward_inputs]
forward_output_names = [item["name"] for item in forward_outputs]
backward_input_names = [item["name"] for item in backward_inputs]
assert len(backward_input_names) <= len(forward_input_names) + 2 * len(
forward_output_names
), f"{op} has too many inputs."
def validate_backward_outputs(op, forward_inputs, backward_outputs):
if op in ['fused_attention_grad']:
return
assert len(backward_outputs) <= len(forward_inputs), (
f"{op} has too many outputs"
)
def cross_validate(ops):
for name, op in ops.items():
if "forward" in op:
fw_call = op["forward"]
fw_name = fw_call["name"]
if fw_name not in ops:
print(
f"Something Wrong here, this backward op ({name})'s forward op ({fw_name}) does not exist."
)
else:
fw_op = ops[fw_name]
if "backward" not in fw_op or fw_op["backward"] is None:
print(
f"Something Wrong here, {name}'s forward op ({fw_name}) does not claim {name} as its backward."
)
else:
assert fw_op["backward"] == name, (
f"{name}: backward and forward name mismatch"
)
assert len(fw_call["inputs"]) <= len(fw_op["inputs"]), (
f"{name}: forward call has more inputs than the op "
)
for input, input_ in zip(fw_call["inputs"], fw_op["inputs"]):
assert input["typename"] == input_["typename"], (
f"type mismatch in {name} and {fw_name}"
)
assert len(fw_call["attrs"]) <= len(fw_op["attrs"]), (
f"{name}: forward call has more attrs than the op "
)
for attr, attr_ in zip(fw_call["attrs"], fw_op["attrs"]):
if attr["typename"] == "Scalar":
# special case for Scalar, fw_call can omit the type
assert re.match(
r"Scalar(\(\w+\))*", attr_["typename"]
), f"type mismatch in {name} and {fw_name}"
else:
assert attr["typename"] == attr_["typename"], (
f"type mismatch in {name} and {fw_name}"
)
assert len(fw_call["outputs"]) == len(fw_op["outputs"]), (
f"{name}: requires outputs number of fw_call == fw_op, but received {fw_call['outputs']} != {fw_op['outputs']}"
)
for output, output_ in zip(
fw_call["outputs"], fw_op["outputs"]
):
assert output["typename"] == output_["typename"], (
f"type mismatch in {name} and {fw_name}"
)