Skip to content

Commit 2afdd93

Browse files
authored
Codegen: Support empty selected operator sets (#21548)
Ensure generated registration sources still include the required function header when there are no selected kernels. Also keep empty selected operator YAML compatible with torchgen by emitting an empty et_kernel_metadata map instead of null. Tested with two tests: - Codegen a library where the selected ops don't match the kernel library, causing silent dropping of those selected ops. - Codegen a library with no selected ops. The intention is to avoid having to generate the selected operator yaml at configure time to decide whether to generate and include the lib, simplifying cmake logic. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani --------- Signed-off-by: Erik Lundell <erik.lundell@arm.com>
1 parent b61ebdd commit 2afdd93

4 files changed

Lines changed: 162 additions & 3 deletions

File tree

codegen/gen.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
16
from __future__ import annotations
27

38
import argparse
@@ -353,6 +358,7 @@ def key_func(
353358
filename,
354359
items,
355360
key_fn=key_func,
361+
base_env={"fn_header": header if not items else []},
356362
env_callable=lambda unbox_kernel_entry: {
357363
"unboxed_kernels": [
358364
ComputeCodegenUnboxedKernels(

codegen/test/targets.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def define_common_targets():
1313
package_style = "inplace",
1414
deps = [
1515
"//executorch/codegen:gen_lib",
16+
"//executorch/codegen/tools:gen_oplist_lib",
1617
"fbsource//third-party/pypi/expecttest:expecttest",
1718
],
1819
external_deps = [

codegen/test/test_executorch_gen.py

Lines changed: 153 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
22
# All rights reserved.
3+
# Copyright 2026 Arm Limited and/or its affiliates.
34
#
45
# This source code is licensed under the BSD-style license found in the
56
# LICENSE file in the root directory of this source tree.
67

78
from __future__ import annotations
89

910
import os
11+
import sys
1012
import tempfile
1113
import unittest
1214

15+
from argparse import Namespace
16+
from pathlib import Path
17+
18+
import executorch.codegen.tools.gen_oplist as gen_oplist
19+
1320
import yaml
1421
from executorch.codegen.gen import (
1522
ComputeCodegenUnboxedKernels,
1623
gen_functions_declarations,
24+
gen_unboxing,
1725
parse_yaml_files,
1826
translate_native_yaml,
1927
)
2028

21-
from executorch.codegen.model import ETKernelIndex, ETKernelKey
22-
from torchgen.gen import LineLoader
29+
from executorch.codegen.model import ETKernelIndex, ETKernelKey, ETParsedYaml
30+
from torchgen.gen import get_custom_build_selector, LineLoader, make_file_manager
2331
from torchgen.model import (
2432
BackendIndex,
2533
BackendMetadata,
@@ -455,6 +463,149 @@ def test_aten_lib_method_variant(self) -> None:
455463
)
456464

457465

466+
class TestGenUnboxing(unittest.TestCase):
467+
EXPECTED_EMPTY_SELECTION_CPP = [
468+
"""
469+
#include "NativeFunctions.h" // Generated Function import headers
470+
""",
471+
"""
472+
static Kernel kernels_to_register[] = {
473+
// Generated kernels
474+
};
475+
""",
476+
]
477+
478+
def _write_codegen_inputs(self, temp_dir: str) -> tuple[str, str, str]:
479+
aten_yaml_path = os.path.join(temp_dir, "native_functions.yaml")
480+
with open(aten_yaml_path, "w") as f:
481+
f.write(TEST_YAML)
482+
included_ops_yaml_path = os.path.join(temp_dir, "included_ops.yaml")
483+
with open(included_ops_yaml_path, "w") as f:
484+
f.write(
485+
"""
486+
- op: add.out
487+
dispatch:
488+
CPU: torch::executor::add_out_kernel
489+
490+
- op: mul.out
491+
dispatch:
492+
CPU: torch::executor::mul_out_kernel
493+
"""
494+
)
495+
tags_yaml_path = os.path.join(temp_dir, "tags.yaml")
496+
with open(tags_yaml_path, "w") as f:
497+
f.write(
498+
"""
499+
- tag: core
500+
desc: test
501+
"""
502+
)
503+
return aten_yaml_path, included_ops_yaml_path, tags_yaml_path
504+
505+
def _assert_selection_generates_empty_buildable_cpp(
506+
self,
507+
temp_dir: str,
508+
selector: SelectiveBuilder,
509+
parsed_yaml: ETParsedYaml,
510+
) -> None:
511+
self.assertEqual([], parsed_yaml.native_functions)
512+
main_module = sys.modules["__main__"]
513+
old_main_file = getattr(main_module, "__file__", None)
514+
main_module.__file__ = __file__
515+
try:
516+
options = Namespace(
517+
source_path=str(Path(__file__).parents[1]),
518+
install_dir=temp_dir,
519+
dry_run=False,
520+
)
521+
gen_unboxing(
522+
native_functions=parsed_yaml.native_functions,
523+
cpu_fm=make_file_manager(options=options),
524+
selector=selector,
525+
use_aten_lib=False,
526+
kernel_index=parsed_yaml.kernel_index,
527+
manual_registration=False,
528+
)
529+
530+
actual_cpp = (
531+
Path(temp_dir) / "RegisterCodegenUnboxedKernelsEverything.cpp"
532+
).read_text()
533+
finally:
534+
if old_main_file is None:
535+
delattr(main_module, "__file__")
536+
else:
537+
main_module.__file__ = old_main_file
538+
539+
for expected in self.EXPECTED_EMPTY_SELECTION_CPP:
540+
self.assertIn(expected, actual_cpp)
541+
self.assertNotIn("${", actual_cpp)
542+
543+
def test_empty_operator_selection_generates_buildable_cpp(self) -> None:
544+
with tempfile.TemporaryDirectory() as temp_dir:
545+
(
546+
aten_yaml_path,
547+
included_ops_yaml_path,
548+
tags_yaml_path,
549+
) = self._write_codegen_inputs(temp_dir)
550+
selected_ops_yaml_path = os.path.join(temp_dir, "selected_operators.yaml")
551+
gen_oplist.main([f"--output_path={selected_ops_yaml_path}"])
552+
selector = get_custom_build_selector(None, selected_ops_yaml_path)
553+
parsed_yaml, _ = parse_yaml_files(
554+
aten_yaml_path=aten_yaml_path,
555+
tags_yaml_path=tags_yaml_path,
556+
native_yaml_path=included_ops_yaml_path,
557+
custom_ops_yaml_path=None,
558+
selector=selector,
559+
use_aten_lib=False,
560+
)
561+
562+
self._assert_selection_generates_empty_buildable_cpp(
563+
temp_dir, selector, parsed_yaml
564+
)
565+
566+
def test_operator_selection_from_missing_library_generates_buildable_cpp(
567+
self,
568+
) -> None:
569+
with tempfile.TemporaryDirectory() as temp_dir:
570+
(
571+
aten_yaml_path,
572+
included_ops_yaml_path,
573+
tags_yaml_path,
574+
) = self._write_codegen_inputs(temp_dir)
575+
selector = SelectiveBuilder.from_yaml_dict(
576+
{
577+
"operators": {
578+
"not_included::op.out": {
579+
"is_root_operator": True,
580+
"is_used_for_training": True,
581+
"include_all_overloads": False,
582+
"debug_info": [],
583+
},
584+
},
585+
"custom_classes": [],
586+
"build_features": [],
587+
"include_all_non_op_selectives": False,
588+
"include_all_operators": False,
589+
"kernel_metadata": {},
590+
"et_kernel_metadata": {
591+
"not_included::op.out": ["default"],
592+
},
593+
}
594+
)
595+
parsed_yaml, _ = parse_yaml_files(
596+
aten_yaml_path=aten_yaml_path,
597+
tags_yaml_path=tags_yaml_path,
598+
native_yaml_path=included_ops_yaml_path,
599+
custom_ops_yaml_path=None,
600+
selector=selector,
601+
use_aten_lib=False,
602+
)
603+
604+
self._assert_selection_generates_empty_buildable_cpp(
605+
temp_dir, selector, parsed_yaml
606+
)
607+
608+
458609
class TestComputeCodegenUnboxedKernels(unittest.TestCase):
459610
def setUp(self) -> None:
460611
(

codegen/tools/gen_oplist.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
22
# All rights reserved.
3+
# Copyright 2026 Arm Limited and/or its affiliates.
34
#
45
# This source code is licensed under the BSD-style license found in the
56
# LICENSE file in the root directory of this source tree.
@@ -185,7 +186,7 @@ def _dump_yaml(
185186
output["include_all_non_op_selectives"] = False
186187
output["include_all_operators"] = include_all_operators
187188
output["kernel_metadata"] = {}
188-
output["et_kernel_metadata"] = et_kernel_metadata
189+
output["et_kernel_metadata"] = et_kernel_metadata or {}
189190
with open(output_path, "wb") as out_file:
190191
out_file.write(
191192
yaml.safe_dump(

0 commit comments

Comments
 (0)