Files
paddlepaddle--paddle/test/dygraph_to_static/dygraph_to_static_utils.py
T
2026-07-13 12:40:42 +08:00

446 lines
12 KiB
Python

# Copyright (c) 2023 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 importlib
import inspect
import logging
import sys
import unittest
from contextlib import contextmanager
from enum import Flag, auto
from functools import wraps
from pathlib import Path
from typing import TypeAlias
import paddle
from paddle import set_flags
from paddle.jit.api import sot_mode_guard
from paddle.jit.dy2static.utils import (
ENV_ENABLE_CINN_IN_DY2ST,
cinn_is_available,
)
from paddle.jit.sot.opcode_translator.executor.executor_cache import (
OpcodeExecutorCache,
)
from paddle.jit.sot.utils.envs import min_graph_size_guard
from paddle.utils.environments import (
BooleanEnvironmentVariable,
EnvironmentVariableGuard,
)
"""
# Usage:
class MyTest(Dy2StTestBase):
@set_to_static_mode(
ToStaticMode.AST | ToStaticMode.SOT
)
@set_backend_mode(BackendMode.PHI | BackendMode.CINN)
def test_case1(self):
raise ValueError("MyTest 1")
def test_case2(self):
raise ValueError("MyTest 2")
class MyTest2(MyTest):
def test_case1(self):
raise ValueError("MyTest2 1")
"""
ENV_EXE_SEQUENTIAL_RUN = BooleanEnvironmentVariable(
"FLAGS_new_executor_sequential_run", False
)
ENV_ENABLE_DY2ST_UT_DEBUG_MODE = BooleanEnvironmentVariable(
"ENABLE_DY2ST_UT_DEBUG_MODE", False
)
logger = logging.getLogger("Dygraph to static utils")
logger.setLevel(
logging.WARNING
if not ENV_ENABLE_DY2ST_UT_DEBUG_MODE.get()
else logging.DEBUG
)
class ToStaticMode(Flag):
AST = auto()
SOT = auto()
# SOT with MIN_GRAPH_SIZE=10, we only test SOT_MGS10 + LEGACY_IR to avoid regression
SOT_MGS10 = auto()
def lower_case_name(self):
return self.name.lower()
class BackendMode(Flag):
PHI = auto()
CINN = auto()
def lower_case_name(self):
return self.name.lower()
ModeTuple: TypeAlias = tuple[ToStaticMode, BackendMode]
DEFAULT_TO_STATIC_MODE = (
ToStaticMode.AST | ToStaticMode.SOT | ToStaticMode.SOT_MGS10
)
DEFAULT_BACKEND_MODE = BackendMode.PHI | BackendMode.CINN
VALID_MODES = [
(ToStaticMode.AST, BackendMode.PHI),
(ToStaticMode.SOT, BackendMode.PHI),
(ToStaticMode.SOT_MGS10, BackendMode.PHI),
]
if cinn_is_available():
VALID_MODES.append(
(ToStaticMode.SOT, BackendMode.CINN),
) # For default mode, we test SOT+CINN
DISABLED_TO_STATIC_TEST_FILES = {
ToStaticMode.AST: [],
ToStaticMode.SOT: [],
ToStaticMode.SOT_MGS10: [],
}
DISABLED_BACKEND_TEST_FILES = {
BackendMode.PHI: [],
BackendMode.CINN: [],
}
def to_ast_test(fn):
"""
convert run AST
"""
@wraps(fn)
def ast_impl(*args, **kwargs):
logger.info("[AST] running AST")
with sot_mode_guard(False):
fn(*args, **kwargs)
return ast_impl
def to_sot_test(fn):
"""
convert run SOT
"""
@wraps(fn)
def sot_impl(*args, **kwargs):
logger.info("[SOT] running SOT (MIN_GRAPH_SIZE=0)")
OpcodeExecutorCache().clear()
with (
sot_mode_guard(True),
min_graph_size_guard(0),
):
fn(*args, **kwargs)
return sot_impl
def to_sot_mgs10_test(fn):
"""
convert run SOT and MIN_GRAPH_SIZE=10
"""
@wraps(fn)
def sot_mgs10_impl(*args, **kwargs):
logger.info("[SOT_MGS10] running SOT (MIN_GRAPH_SIZE=10)")
OpcodeExecutorCache().clear()
with (
sot_mode_guard(True),
min_graph_size_guard(10),
):
fn(*args, **kwargs)
return sot_mgs10_impl
def to_phi_test(fn):
@wraps(fn)
def phi_impl(*args, **kwargs):
logger.info("[PHI] running phi")
with EnvironmentVariableGuard(ENV_ENABLE_CINN_IN_DY2ST, False):
fn(*args, **kwargs)
return phi_impl
def to_cinn_test(fn):
@wraps(fn)
def cinn_impl(*args, **kwargs):
logger.info("[CINN] running cinn")
with EnvironmentVariableGuard(ENV_ENABLE_CINN_IN_DY2ST, True):
fn(*args, **kwargs)
return cinn_impl
# Metaclass and BaseClass
class Dy2StTestMeta(type):
TO_STATIC_HANDLER_MAP = {
ToStaticMode.AST: to_ast_test,
ToStaticMode.SOT: to_sot_test,
ToStaticMode.SOT_MGS10: to_sot_mgs10_test,
}
BACKEND_HANDLER_MAP = {
BackendMode.PHI: to_phi_test,
BackendMode.CINN: to_cinn_test,
}
def __new__(cls, name, bases, attrs):
module_name = attrs["__module__"]
filepath = sys.modules[module_name].__file__
assert filepath
filename = Path(filepath).stem
new_attrs = {}
original_test_cases = {
key: value
for key, value in attrs.items()
if key.startswith("test") and inspect.isfunction(value)
}
logger.info(f"[creating {name}]")
new_attrs.update(
{
key: value
for key, value in attrs.items()
if key not in original_test_cases
}
)
for fn_name, fn in original_test_cases.items():
logger.info(f"Generating {fn_name}")
fn_disabled_test_cases = getattr(fn, "disabled_test_cases", [])
logger.info(f"fn_disabled_test_cases: {fn_disabled_test_cases}")
# Disable inherited test cases
for base in bases:
for attr in dir(base):
if attr.startswith(f"{fn_name}__"):
new_attrs[attr] = None
# Get all valid test cases with to_static_mode and ir_mode
mode_tuples = Dy2StTestMeta.get_all_test_mode_tuples(fn)
# Filter out invalid test cases
mode_tuples = list(
filter(
# Filter out disabled test cases by decorator
lambda mode_tuple: (
not Dy2StTestMeta.is_disabled_by_attr(
fn_disabled_test_cases,
mode_tuple,
)
# Filter out disabled test cases by file
and not Dy2StTestMeta.is_disabled_by_file(
filename,
mode_tuple,
)
# Skip invalid test cases
and mode_tuple in VALID_MODES
),
mode_tuples,
)
)
# Generate all test cases
for mode_tuple in mode_tuples:
new_attrs[Dy2StTestMeta.test_case_name(fn_name, mode_tuple)] = (
Dy2StTestMeta.convert_test_case(fn, mode_tuple)
)
return type.__new__(cls, name, bases, new_attrs)
@staticmethod
def get_all_test_mode_tuples(fn):
fn_to_static_modes = getattr(
fn, "to_static_mode", DEFAULT_TO_STATIC_MODE
)
fn_backend_modes = getattr(fn, "backend_mode", DEFAULT_BACKEND_MODE)
logger.info(f"fn_to_static_modes: {fn_to_static_modes}")
logger.info(f"fn_backend_modes: {fn_backend_modes}")
return [
(to_static_mode, backend_mode)
for to_static_mode in ToStaticMode
for backend_mode in BackendMode
if to_static_mode & fn_to_static_modes
and backend_mode & fn_backend_modes
]
@staticmethod
def is_disabled_by_attr(
fn_disabled_test_cases: list[ModeTuple], mode_tuple: ModeTuple
):
to_static_mode, backend_mode = mode_tuple
for (
disabled_to_static_mode,
disabled_backend_mode,
) in fn_disabled_test_cases:
if (
to_static_mode & disabled_to_static_mode
and backend_mode & disabled_backend_mode
):
return True
return False
@staticmethod
def is_disabled_by_file(
filename: str,
mode_tuple: ModeTuple,
):
to_static_mode, backend_mode = mode_tuple
if (
filename in DISABLED_TO_STATIC_TEST_FILES[to_static_mode]
or filename in DISABLED_BACKEND_TEST_FILES[backend_mode]
):
return True
return False
@staticmethod
def test_case_name(original_name: str, mode_tuple: ModeTuple):
to_static_mode, backend_mode = mode_tuple
return f"{original_name}__{to_static_mode.lower_case_name()}_{backend_mode.lower_case_name()}"
@staticmethod
def convert_test_case(fn, mode_tuple: ModeTuple):
to_static_mode, backend_mode = mode_tuple
fn = Dy2StTestMeta.BACKEND_HANDLER_MAP[backend_mode](fn)
fn = Dy2StTestMeta.TO_STATIC_HANDLER_MAP[to_static_mode](fn)
return fn
class Dy2StTestBase(unittest.TestCase, metaclass=Dy2StTestMeta):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Base decorators
def set_to_static_mode(mode: ToStaticMode):
def decorator(fn):
fn.to_static_mode = mode
return fn
return decorator
def set_backend_mode(mode: BackendMode):
def decorator(fn):
fn.backend_mode = mode
return fn
return decorator
def disable_test_case(flags: tuple[ToStaticMode, BackendMode]):
def decorator(fn):
disabled_test_cases = getattr(fn, "disabled_test_cases", [])
disabled_test_cases.append(flags)
fn.disabled_test_cases = disabled_test_cases
return fn
return decorator
# Sugar decorators
# These decorators can be simply composed by base decorators
def test_ast_only(fn):
fn = set_to_static_mode(ToStaticMode.AST)(fn)
return fn
def test_sot_only(fn):
fn = set_to_static_mode(ToStaticMode.SOT | ToStaticMode.SOT_MGS10)(fn)
return fn
def test_phi_only(fn):
fn = set_backend_mode(BackendMode.PHI)(fn)
return fn
def test_cinn_only(fn):
fn = set_backend_mode(BackendMode.CINN)(fn)
return fn
# Some decorators for save CI time
def test_default_mode_only(fn):
# Some unittests has high time complexity, we only test them with default mode
fn = set_to_static_mode(ToStaticMode.SOT)(fn)
fn = set_backend_mode(BackendMode.PHI)(fn)
return fn
def test_sot_mgs0_only(fn):
fn = set_to_static_mode(ToStaticMode.SOT)(fn)
return fn
# For debug
def show_all_test_cases(test_class):
logger.info(f"[showing {test_class.__name__}]")
for attr in dir(test_class):
if attr.startswith("test"):
fn = getattr(test_class, attr)
logger.info(f"{attr}: {fn}")
# Other utilities
def import_module_from_path(module_name, module_path):
"""A better way to import module from other directory than using sys.path.append"""
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def import_legacy_test_utils():
test_root = Path(__file__).parent.parent
legacy_test_utils_path = test_root / "legacy_test/utils.py"
legacy_test_utils = import_module_from_path(
"legacy_test_utils", legacy_test_utils_path
)
return legacy_test_utils
legacy_test_utils = import_legacy_test_utils()
dygraph_guard = legacy_test_utils.dygraph_guard
static_guard = legacy_test_utils.static_guard
@contextmanager
def enable_to_static_guard(flag: bool):
program_translator = paddle.jit.api.ProgramTranslator()
original_flag_value = program_translator.enable_to_static
program_translator.enable(flag)
try:
yield
finally:
program_translator.enable(original_flag_value)
@contextmanager
def exe_sequential_run_guard(value: bool):
exe_sequential_run_flag = ENV_EXE_SEQUENTIAL_RUN.name
original_flag_value = paddle.get_flags(exe_sequential_run_flag)[
exe_sequential_run_flag
]
with EnvironmentVariableGuard(ENV_EXE_SEQUENTIAL_RUN, value):
try:
set_flags({exe_sequential_run_flag: value})
yield
finally:
set_flags({exe_sequential_run_flag: original_flag_value})