Skip to content

Commit 87a242b

Browse files
committed
feat(build): select operator implementations from ops.json
1 parent 53bf6b7 commit 87a242b

11 files changed

Lines changed: 859 additions & 43 deletions

docs/build.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,60 @@ entry is `python -m pip install` with CMake options passed through
2727
| `INFINI_OPS_BUILD_DOCS` | Enable the Doxygen documentation target. | `OFF` |
2828
| `INFINI_RT_ROOT` | InfiniRT install prefix containing `include/` and `lib/`. | `$INFINI_RT_ROOT` |
2929
| `INFINI_OPS_SMOKE_BUILD` | Build only the smoke-test operator subset. | `OFF` |
30-
| `INFINI_OPS_OPS` | Comma- or semicolon-separated operator allowlist. | empty |
30+
| `INFINI_OPS_OPS` | Comma- or semicolon-separated operator allowlist, or a path to an `ops.json` implementation selection. | empty |
3131
| `INFINI_OPS_TORCH_OPS` | Comma- or semicolon-separated ATen operator allowlist. | empty |
3232

33+
An `ops.json` file selects operators and implementation slots with a top-level
34+
operator mapping:
35+
36+
```json
37+
{
38+
"add": {
39+
"implementations": "all"
40+
},
41+
"argmax": {
42+
"implementations": [8]
43+
},
44+
"top_k_top_p_sampling_from_logits": {
45+
"implementations": [16]
46+
}
47+
}
48+
```
49+
50+
`"all"` keeps every available implementation for the operator. An integer
51+
array keeps exactly those slots. Slots range from 0 through 31. The selection
52+
is a set, not a priority order; the default dispatch selects the smallest
53+
active slot. The selection controls generated wrappers, generated slot-8 ATen
54+
implementations, and linked
55+
provider resolution. Unselected linked providers do not require their external
56+
libraries to be installed.
57+
58+
Pass the file explicitly with
59+
`-DINFINI_OPS_OPS=/path/to/ops.json`. For compatibility,
60+
`${PROJECT_SOURCE_DIR}/ops.json` is read automatically when present. Relative
61+
implementation header paths in legacy configurations are resolved from
62+
`${PROJECT_SOURCE_DIR}`. An explicit inline `INFINI_OPS_OPS` allowlist takes
63+
precedence over an implicit `${PROJECT_SOURCE_DIR}/ops.json`. When
64+
`INFINI_OPS_TORCH_OPS` and an explicit JSON selection are both set, generated
65+
ATen ops use their intersection. The string and string-array values supported
66+
by the current generator remain available for checked-in implementation
67+
headers. Structured descriptors preserve an explicit backend name, including
68+
for implementations outside the standard backend directory layout. Generated
69+
implementation header paths are not supported:
70+
71+
```json
72+
{
73+
"add": "src/native/cpu/ops/add/add.h",
74+
"gemm": ["src/native/cpu/ops/gemm/gemm.h"],
75+
"custom_add": [
76+
{
77+
"path": "custom/add.h",
78+
"backend": "custom"
79+
}
80+
]
81+
}
82+
```
83+
3384
Only one GPU backend should be enabled in a build. CPU may be enabled with the
3485
selected accelerator backend.
3586

docs/linked-operators.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ cmake -S . -B build \
8686
-DINFINI_OPS_OPS=silu_and_mul
8787
```
8888

89+
To resolve only selected linked implementation slots, pass an `ops.json` file
90+
through `INFINI_OPS_OPS`. The resolver reads each linked provider's slot from
91+
its sibling C++ header before locating external libraries, so an unselected
92+
provider does not add a package or shared-library dependency. See
93+
[Build configuration](build.md) for the file format.
94+
8995
The `torch` transport uses the installed PyTorch C++ headers and libraries for
9096
`at::Tensor`, but it does not enable the standard `src/torch` operator backend.
9197
Provider and PyTorch C++ ABIs must match. Configuration fails before compilation

scripts/generate_torch_ops.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
import yaml
3737

3838
_SCRIPTS_DIR = pathlib.Path(__file__).resolve().parent
39+
sys.path.insert(0, str(_SCRIPTS_DIR))
40+
import ops_config # noqa: E402
41+
3942
_REPO_ROOT = _SCRIPTS_DIR.parent
4043
_OPS_YAML_PATH = _SCRIPTS_DIR / "torch_ops.yaml"
4144
_BASE_DIR = _REPO_ROOT / "src" / "base"
@@ -1644,13 +1647,42 @@ def _emit(name: str, ops: list[Op], *, emit_base: bool) -> set[pathlib.Path]:
16441647
return emitted_paths
16451648

16461649

1650+
def _select_op_names(cli_ops, default_ops, config):
1651+
if config is None:
1652+
return cli_ops or default_ops
1653+
1654+
aten_names_by_public_name = collections.defaultdict(list)
1655+
for op_name in default_ops:
1656+
aten_names_by_public_name[_public_op_name(op_name)].append(op_name)
1657+
1658+
selected_public_names = ops_config.torch_op_names(
1659+
config, aten_names_by_public_name, _PYTORCH_SLOT
1660+
)
1661+
selected = [
1662+
aten_name
1663+
for public_name in selected_public_names
1664+
for aten_name in aten_names_by_public_name.get(public_name, (public_name,))
1665+
]
1666+
1667+
if cli_ops:
1668+
allowed = set(cli_ops)
1669+
selected = [op_name for op_name in selected if op_name in allowed]
1670+
1671+
return selected
1672+
1673+
16471674
def main() -> int:
16481675
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
16491676
parser.add_argument(
16501677
"--ops",
16511678
nargs="*",
16521679
help="Override the op allowlist. If omitted, reads `scripts/torch_ops.yaml`.",
16531680
)
1681+
parser.add_argument(
1682+
"--ops-config",
1683+
type=pathlib.Path,
1684+
help="Path to an `ops.json` operator and implementation selection.",
1685+
)
16541686
parser.add_argument(
16551687
"--pytorch-version",
16561688
default=os.environ.get("INFINI_OPS_PYTORCH_VERSION", _DEFAULT_PYTORCH_VERSION),
@@ -1666,7 +1698,9 @@ def main() -> int:
16661698
global _CLANG_FORMAT
16671699
_CLANG_FORMAT = _find_clang_format()
16681700

1669-
op_names = args.ops or yaml.safe_load(_OPS_YAML_PATH.read_text())
1701+
default_ops = yaml.safe_load(_OPS_YAML_PATH.read_text())
1702+
config = ops_config.load_ops_config(args.ops_config) if args.ops_config else None
1703+
op_names = _select_op_names(args.ops, default_ops, config)
16701704
aten_entries = _load_aten_entries(args.pytorch_version)
16711705

16721706
skipped: list[tuple[str, str]] = []

scripts/generate_wrappers.py

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
import concurrent.futures
33
import dataclasses
44
import functools
5-
import json
65
import os
76
import pathlib
87
import re
98
import shutil
109
import subprocess
10+
import sys
1111
import textwrap
1212

13+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
14+
import ops_config # noqa: E402
15+
1316
try:
1417
import clang.cindex
1518
from clang.cindex import CursorKind
@@ -1848,6 +1851,52 @@ def _filter_ops(ops, op_allowlist, *, strict=False):
18481851
return {op_name: ops[op_name] for op_name in op_allowlist if op_name in ops}
18491852

18501853

1854+
def _select_ops_from_config(ops, config, config_path):
1855+
selected = {}
1856+
1857+
for op_name, selection in config.items():
1858+
headers = selection["headers"]
1859+
1860+
if headers is not None:
1861+
selected[op_name] = [
1862+
_implementation_from_json(header) for header in headers
1863+
]
1864+
continue
1865+
1866+
if op_name not in ops:
1867+
raise ValueError(
1868+
f"{config_path}: operator {op_name!r} is not available for "
1869+
"the active devices"
1870+
)
1871+
1872+
slots = selection["implementations"]
1873+
1874+
if slots is None:
1875+
selected[op_name] = ops[op_name]
1876+
continue
1877+
1878+
headers_by_slot = {}
1879+
1880+
for implementation in ops[op_name]:
1881+
slot = ops_config.implementation_slot(implementation.path)
1882+
headers_by_slot.setdefault(slot, []).append(implementation)
1883+
1884+
missing = [slot for slot in slots if slot not in headers_by_slot]
1885+
1886+
if missing:
1887+
formatted = ", ".join(str(slot) for slot in missing)
1888+
raise ValueError(
1889+
f"{config_path}: operator {op_name!r} has no active "
1890+
f"implementation at slot(s) {formatted}"
1891+
)
1892+
1893+
selected[op_name] = [
1894+
implementation for slot in slots for implementation in headers_by_slot[slot]
1895+
]
1896+
1897+
return selected
1898+
1899+
18511900
def _get_all_ops(
18521901
devices,
18531902
with_torch=False,
@@ -2084,6 +2133,11 @@ def _dispatch_gen_batch_size():
20842133
type=str,
20852134
help="Operator allowlist to generate. Accepts names separated by spaces or commas.",
20862135
)
2136+
parser.add_argument(
2137+
"--ops-config",
2138+
type=pathlib.Path,
2139+
help="Path to an `ops.json` operator and implementation selection.",
2140+
)
20872141
parser.add_argument(
20882142
"--strict-ops",
20892143
action="store_true",
@@ -2101,25 +2155,18 @@ def _dispatch_gen_batch_size():
21012155
for directory in (_BINDINGS_DIR, _GENERATED_SRC_DIR, _INCLUDE_DIR):
21022156
directory.mkdir(parents=True, exist_ok=True)
21032157

2104-
ops_json = pathlib.Path("ops.json")
2158+
config_path = args.ops_config
2159+
ops = _get_all_ops(
2160+
args.devices,
2161+
with_torch=args.with_torch,
2162+
with_ninetoothed=args.with_ninetoothed,
2163+
with_linked=args.with_linked,
2164+
with_triton=args.with_triton,
2165+
)
21052166

2106-
if ops_json.exists():
2107-
raw_ops = json.loads(ops_json.read_text())
2108-
ops = {
2109-
op_name: [
2110-
_implementation_from_json(implementation)
2111-
for implementation in implementations
2112-
]
2113-
for op_name, implementations in raw_ops.items()
2114-
}
2115-
else:
2116-
ops = _get_all_ops(
2117-
args.devices,
2118-
with_torch=args.with_torch,
2119-
with_ninetoothed=args.with_ninetoothed,
2120-
with_linked=args.with_linked,
2121-
with_triton=args.with_triton,
2122-
)
2167+
if config_path is not None:
2168+
config = ops_config.load_ops_config(config_path)
2169+
ops = _select_ops_from_config(ops, config, config_path)
21232170

21242171
ops = _filter_ops(
21252172
ops,

0 commit comments

Comments
 (0)