Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions benchmark/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,10 @@ def run(self):
try:
args, kwargs = self.unpack_to_args_kwargs(input)
metric.shape_detail = self.record_shapes(*args, **kwargs)
if "latency_base" in self.to_bench_metrics:
if (
"latency_base" in self.to_bench_metrics
and not Config.skip_native
):
metric.latency_base = self.get_latency(
self.torch_op, *args, **kwargs
)
Expand All @@ -459,13 +462,30 @@ def run(self):
self.torch_op, *args, **kwargs
)
if "speedup" in self.to_bench_metrics:
metric.speedup = metric.latency_base / metric.latency
if Config.skip_native:
if (
metric.latency_base is not None
and metric.latency is not None
):
metric.speedup = metric.latency_base / metric.latency
else:
metric.speedup = metric.latency_base / metric.latency

if "gbps" in self.to_bench_metrics:
metric.gbps_base = self.get_gbps(
args, latency=metric.latency_base
)
metric.gbps = self.get_gbps(args, latency=metric.latency)
if Config.skip_native:
if metric.latency_base is not None:
metric.gbps_base = self.get_gbps(
args, latency=metric.latency_base
)
if metric.latency is not None:
metric.gbps = self.get_gbps(
args, latency=metric.latency
)
else:
metric.gbps_base = self.get_gbps(
args, latency=metric.latency_base
)
metric.gbps = self.get_gbps(args, latency=metric.latency)

if "tflops" in self.to_bench_metrics:
metric.tflops = (
Expand All @@ -489,8 +509,15 @@ def run(self):
mode=Config.mode.value,
result=metrics,
)
if Config.native_baseline_skip_reason:
result.native_baseline_skip_reason = Config.native_baseline_skip_reason
print(result)
update_result(self.op_name, asdict(result))
result_dict = asdict(result)
if Config.native_baseline_skip_reason:
result_dict["native_baseline_skip_reason"] = (
Config.native_baseline_skip_reason
)
update_result(self.op_name, result_dict)
emit_record_logger(result.to_json())


Expand Down
99 changes: 95 additions & 4 deletions benchmark/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"tryfirst",
"trylast",
)
BENCHMARK_CONTROL_MARKS = ("skip_native",)
NON_OPERATOR_MARKS = BUILTIN_MARKS + BENCHMARK_CONTROL_MARKS
REGISTERED_MARKS = []
TEST_RESULTS = {}
REPORT_FILE = "benchmark_result.json"
Expand Down Expand Up @@ -95,6 +97,67 @@ def __init__(self):
self.shape_file = os.path.join(os.path.dirname(__file__), "core_shapes.yaml")
self.query = False
self.parallel = 0
self.skip_native = False
self.native_baseline_skip_reason = None


def _get_native_baseline_skip_reason(marker, current_vendor):
if marker.args:
raise pytest.UsageError(
"skip_native only accepts the keyword arguments 'vendors' and 'reason'"
)

unexpected = set(marker.kwargs) - {"vendors", "reason"}
if unexpected:
raise pytest.UsageError(
f"skip_native got unexpected argument(s): {', '.join(sorted(unexpected))}"
)

vendors = marker.kwargs.get("vendors")
if isinstance(vendors, str):
vendors = (vendors,)
elif isinstance(vendors, (list, tuple, set, frozenset)):
vendors = tuple(vendors)
else:
raise pytest.UsageError(
"skip_native requires 'vendors' to be a vendor name or a collection of vendor names"
)

if not vendors or not all(
isinstance(vendor, str) and vendor.strip() for vendor in vendors
):
raise pytest.UsageError(
"skip_native requires at least one non-empty vendor name"
)

reason = marker.kwargs.get("reason")
if not isinstance(reason, str) or not reason.strip():
raise pytest.UsageError("skip_native requires a non-empty 'reason'")

normalized_vendors = {vendor.strip().lower() for vendor in vendors}
if current_vendor.lower() not in normalized_vendors:
return None
return reason.strip()


def _deactivate_inactive_native_marker(item, current_vendor):
marker = item.get_closest_marker("skip_native")
if marker is None:
return

try:
reason = _get_native_baseline_skip_reason(marker, current_vendor)
except pytest.UsageError:
# Keep invalid markers visible so the setup fixture reports the error.
return

if reason is not None:
return

for node in reversed(item.listchain()):
if marker in node.own_markers:
node.own_markers.remove(marker)
return


def pytest_addoption(parser):
Expand Down Expand Up @@ -222,6 +285,11 @@ def pytest_configure(config):
global REPORT_FILE
global REGISTERED_MARKS

config.addinivalue_line(
"markers",
"skip_native(vendors, reason): skip the native benchmark baseline for selected vendors",
)

Config = BenchConfig()

REGISTERED_MARKS = {
Expand Down Expand Up @@ -299,6 +367,25 @@ def clear_function_cache():
torch_device_fn.empty_cache()


@pytest.fixture(scope="function", autouse=True)
def configure_native_baseline(request):
Config.skip_native = False
Config.native_baseline_skip_reason = None
marker = request.node.get_closest_marker("skip_native")
reason = (
_get_native_baseline_skip_reason(marker, vendor_name)
if marker is not None
else None
)
Config.skip_native = reason is not None
Config.native_baseline_skip_reason = reason
try:
yield
finally:
Config.skip_native = False
Config.native_baseline_skip_reason = None


@pytest.fixture(scope="module", autouse=True)
def clear_module_cache():
yield
Expand All @@ -312,7 +399,7 @@ def extract_and_log_op_attributes(request):

# Extract the 'recommended_shapes' attribute from the pytest marker decoration.
for mark in request.node.iter_markers():
if mark.name in BUILTIN_MARKS:
if mark.name in NON_OPERATOR_MARKS:
continue
op_specified_shapes = mark.kwargs.get("recommended_shapes")
shape_desc = mark.kwargs.get("shape_desc", "M, N")
Expand Down Expand Up @@ -353,8 +440,8 @@ def pytest_runtest_makereport(item, call):
out = yield
report = out.get_result()
all_marks = [mark.name for mark in item.iter_markers()]
# exclude builtin marks
marks = [mark for mark in all_marks if mark not in BUILTIN_MARKS]
# exclude pytest and benchmark control marks
marks = [mark for mark in all_marks if mark not in NON_OPERATOR_MARKS]
# Assume the first mark is the operator's ID
opid = marks[0] if marks else item.nodeid
# Set the operator ID for the next function to use
Expand Down Expand Up @@ -403,6 +490,10 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
json.dump(data, f, indent=2, default=str)


def pytest_itemcollected(item):
_deactivate_inactive_native_marker(item, vendor_name)


def pytest_collection_modifyitems(session, config, items):
collect_marks_file = config.getoption("--collect-marks")
if not collect_marks_file:
Expand All @@ -424,7 +515,7 @@ def pytest_collection_modifyitems(session, config, items):
op_marks = [
mark.name
for mark in all_marks
if mark.name not in BUILTIN_MARKS and mark.name not in REGISTERED_MARKS
if mark.name not in NON_OPERATOR_MARKS and mark.name not in REGISTERED_MARKS
]

data["marks"] = op_marks
Expand Down
6 changes: 6 additions & 0 deletions benchmark/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ def __str__(self) -> str:
f"\nOperator: {self.op_name} Performance Test (dtype={self.dtype}, mode={self.mode},"
f"level={self.level})\n"
)
native_baseline_skip_reason = getattr(self, "native_baseline_skip_reason", None)
if native_baseline_skip_reason:
header_title += f"Native baseline: N/A ({native_baseline_skip_reason})\n"
col_names = [
f"{'Status':<10}",
f"{'Torch Latency (ms)':>20}",
Expand Down Expand Up @@ -304,6 +307,9 @@ def to_json(self) -> str:

# Convert to dict and handle tuple serialization for shape_detail
result_dict = asdict(self)
native_baseline_skip_reason = getattr(self, "native_baseline_skip_reason", None)
if native_baseline_skip_reason:
result_dict["native_baseline_skip_reason"] = native_baseline_skip_reason
return json.dumps(result_dict, default=custom_json_encoder)

def to_dict(self) -> dict:
Expand Down
8 changes: 8 additions & 0 deletions benchmark/test_unique_dim.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def _input_fn_dim1(shape, dtype, device):


@pytest.mark.unique_dim
@pytest.mark.skip_native(
vendors=("ascend",),
reason="aten::unique_dim falls back to CPU on Ascend",
)
def test_unique_dim_dim0():
bench = base.GenericBenchmark2DOnly(
input_fn=_input_fn_dim0,
Expand All @@ -50,6 +54,10 @@ def test_unique_dim_dim0():


@pytest.mark.unique_dim
@pytest.mark.skip_native(
vendors=("ascend",),
reason="aten::unique_dim falls back to CPU on Ascend",
)
def test_unique_dim_dim1():
bench = base.GenericBenchmark2DOnly(
input_fn=_input_fn_dim1,
Expand Down
2 changes: 2 additions & 0 deletions src/flag_gems/runtime/backend/_ascend/ops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
from .threshold import threshold, threshold_backward
from .triu import triu
from .unique import _unique2
from .unique_dim import unique_dim
from .upsample_bicubic2d_aa import _upsample_bicubic2d_aa
from .upsample_linear1d_backward import upsample_linear1d_backward
from .upsample_nearest2d import upsample_nearest2d
Expand Down Expand Up @@ -206,6 +207,7 @@
"threshold",
"threshold_backward",
"triu",
"unique_dim",
"upsample_linear1d_backward",
"upsample_nearest2d",
"var_mean",
Expand Down
Loading
Loading