Skip to content

Commit c097d3b

Browse files
authored
Merge branch 'main' into vulkan-compatibility
2 parents 391f139 + 574bfca commit c097d3b

30 files changed

Lines changed: 737 additions & 147 deletions

.github/workflows/build-cadence-runner.yml

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,45 @@ on:
1010
tags:
1111
- ciflow/nightly/*
1212
pull_request:
13+
pull_request_target:
14+
types: [labeled]
1315
workflow_dispatch:
1416

1517
concurrency:
16-
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.event_name == 'workflow_dispatch' }}
18+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.sha }}
1719
cancel-in-progress: true
1820

1921
jobs:
22+
gate:
23+
runs-on: ubuntu-latest
24+
outputs:
25+
run-cadence: ${{ steps.decide.outputs.run }}
26+
steps:
27+
- id: decide
28+
env:
29+
EVENT: ${{ github.event_name }}
30+
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
31+
HAS_CLA: ${{ contains(github.event.pull_request.labels.*.name, 'CLA Signed') }}
32+
HAS_EXPORT: ${{ contains(github.event.pull_request.labels.*.name, 'meta-exported') }}
33+
run: |
34+
run=false
35+
case "${EVENT}" in
36+
push|schedule|workflow_dispatch)
37+
run=true
38+
;;
39+
pull_request)
40+
[ "${IS_FORK}" = "false" ] && run=true
41+
;;
42+
pull_request_target)
43+
if [ "${IS_FORK}" = "true" ] && [ "${HAS_CLA}" = "true" ] && [ "${HAS_EXPORT}" = "true" ]; then
44+
run=true
45+
fi
46+
;;
47+
esac
48+
echo "run=${run}" >> "${GITHUB_OUTPUT}"
49+
2050
cpu-build:
51+
if: github.event_name != 'pull_request_target'
2152
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
2253
permissions:
2354
id-token: write
@@ -44,6 +75,7 @@ jobs:
4475
4576
cpu-test:
4677
needs: cpu-build
78+
if: github.event_name != 'pull_request_target'
4779
permissions:
4880
id-token: write
4981
contents: read
@@ -56,19 +88,23 @@ jobs:
5688
# lives in _xtensa_build.yml. fusion_g3 is omitted until the upstream fusion_g3
5789
# <-> nnlib-FusionG3 API skew is fixed (its runner does not link).
5890
hifi-build:
91+
needs: gate
92+
if: needs.gate.outputs.run-cadence == 'true'
5993
permissions:
6094
id-token: write
6195
contents: read
6296
uses: ./.github/workflows/_xtensa_build.yml
6397
with:
6498
backend: hifi4
65-
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
99+
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.pull_request.head.sha || github.sha }}
66100

67101
vision-build:
102+
needs: gate
103+
if: needs.gate.outputs.run-cadence == 'true'
68104
permissions:
69105
id-token: write
70106
contents: read
71107
uses: ./.github/workflows/_xtensa_build.yml
72108
with:
73109
backend: vision
74-
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
110+
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.pull_request.head.sha || github.sha }}

.github/workflows/mlx.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ jobs:
7979
backends/mlx/test/test_pattern_utils.py \
8080
backends/mlx/test/test_partitioner.py \
8181
backends/mlx/test/test_serialization_dedup.py \
82+
backends/mlx/test/test_slot_recycling.py \
8283
examples/models/gemma4_31b/quant/tests/test_pack_mlx.py \
8384
examples/models/gemma4_31b/tests/test_mlx_pipeline.py \
8485
-v

backends/apple/coreml/runtime/delegate/ETCoreMLModel.mm

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,15 @@ - (nullable instancetype)initWithAsset:(ETCoreMLAsset *)asset
194194
return nil;
195195
}
196196

197-
MLModel *mlModel = [MLModel modelWithContentsOfURL:asset.contentURL
197+
NSURL *contentURL = asset.contentURL;
198+
if (contentURL == nil) {
199+
ETCoreMLLogErrorAndSetNSError(error,
200+
ETCoreMLErrorCorruptedModel,
201+
"asset.contentURL is nil");
202+
return nil;
203+
}
204+
205+
MLModel *mlModel = [MLModel modelWithContentsOfURL:contentURL
198206
configuration:configuration
199207
error:error];
200208
if (!mlModel) {

backends/arm/test/conftest.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ def pytest_configure(config):
2626
pytest._test_options["llama_inputs"] = config.option.llama_inputs # type: ignore[attr-defined]
2727

2828
logging.basicConfig(stream=sys.stdout)
29-
_set_random_seed()
3029

3130

3231
def pytest_collection_modifyitems(config, items):
@@ -79,28 +78,24 @@ def set_random_seed():
7978
ARM_TEST_SEED=3478246 pytest --config-file=/dev/null --verbose -s --color=yes backends/arm/test/ops/test_avg_pool.py -k <TESTCASE>
8079
8180
"""
82-
_set_random_seed()
83-
84-
85-
def _set_random_seed():
8681
import torch
8782

8883
seed_env = os.environ.get("ARM_TEST_SEED", "0")
8984
if seed_env == "RANDOM":
9085
random.seed() # reset seed, in case any other test has fiddled with it
9186
seed = random.randint(0, 2**32 - 1) # nosec B311 - non-crypto seed for tests
9287
torch.manual_seed(seed)
93-
print(f" ARM_TEST_SEED=RANDOM using:{seed} ", end=" ")
9488
elif str.isdigit(seed_env):
9589
seed = int(seed_env)
9690
random.seed(seed)
9791
torch.manual_seed(seed)
98-
print(f" ARM_TEST_SEED={seed} ", end=" ")
9992
else:
10093
raise TypeError(
10194
"ARM_TEST_SEED env variable must be integers or the string RANDOM"
10295
)
10396

97+
print(f" ARM_TEST_SEED={seed} ", end=" ")
98+
10499

105100
# ==== End of Pytest fixtures =====
106101

backends/mlx/builder/program_builder.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,13 @@ def make_tmp_value_slot(self) -> Tuple[str, Slot]:
242242
"""Create a temporary value (SymInt) slot."""
243243
return self.slot_manager.make_tmp_value_slot()
244244

245+
def tmp_scope(self):
246+
"""Context manager scoping temporary slot ids for reuse.
247+
248+
See :meth:`SlotManager.tmp_scope`.
249+
"""
250+
return self.slot_manager.tmp_scope()
251+
245252
def make_or_get_constant(self, name: str, tensor: torch.Tensor) -> Slot:
246253
"""
247254
Creates an extra constant outside of the ExportedProgram state_dict.
@@ -529,7 +536,8 @@ def _process_nodes(self) -> None: # noqa C901
529536

530537
if self.node_info[n].handler is not None:
531538
handler = self.node_info[n].handler
532-
handler(self, n)
539+
with self.tmp_scope():
540+
handler(self, n)
533541
self._mark_supported(n, handler=handler)
534542
continue
535543

@@ -558,7 +566,8 @@ def _process_nodes(self) -> None: # noqa C901
558566
continue
559567

560568
try:
561-
handler(self, n)
569+
with self.tmp_scope():
570+
handler(self, n)
562571
self._mark_supported(n, handler=handler)
563572
except Exception as e:
564573
trace_str = traceback.format_exc()
@@ -688,14 +697,26 @@ def _collect_used_slots(
688697
# Inputs, outputs, mutable buffers - always include
689698
used_slots.add(s)
690699

700+
# Count distinct physical slots. Slots that share (id_space, idx) are the
701+
# same slot reused across disjoint lifetimes (delete-as-you-go reclaim /
702+
# tmp_scope) and are coalesced to a single global id below, so they must
703+
# be counted once. (For non-tensors, SymInt/SymBool share the vid pool.)
704+
#
705+
# NOTE: the key here is (is_tensor, id_space, idx), while
706+
# _create_slot_mappings keys only on (id_space, idx). The two stay
707+
# equivalent only because tids and vids are coalesced in separate passes
708+
# there (is_tensor is constant within each), so this count matches the
709+
# number of distinct global ids per space. Keep the two in sync.
691710
num_tensors: Dict[IdSpace, int] = defaultdict(int)
692711
num_values: Dict[IdSpace, int] = defaultdict(int)
693-
seen: Set[Slot] = set()
712+
seen_keys: Set[Tuple[bool, IdSpace, int]] = set()
694713
for s in used_slots:
695-
if s in seen:
714+
is_tensor = s.id_type == IdType.Tensor
715+
key = (is_tensor, s.id_space, s.idx)
716+
if key in seen_keys:
696717
continue
697-
seen.add(s)
698-
if s.id_type == IdType.Tensor:
718+
seen_keys.add(key)
719+
if is_tensor:
699720
num_tensors[s.id_space] += 1
700721
else:
701722
num_values[s.id_space] += 1
@@ -719,19 +740,28 @@ def _create_slot_mappings(
719740
IdSpace.Temp: 4,
720741
}
721742

743+
# Coalesce slots that share (id_space, idx) to a single global id. Such
744+
# slots are the same physical slot reused across disjoint lifetimes
745+
# (delete-as-you-go reclaim / tmp_scope), so they must map to the same
746+
# global Tid/Vid. Sorting by (id_space, idx) keeps per-space id ranges
747+
# contiguous, matching the counts from _collect_used_slots.
748+
def _coalesce(slots: List[Slot]) -> Dict[Slot, int]:
749+
mapping: Dict[Slot, int] = {}
750+
key_to_global: Dict[Tuple[IdSpace, int], int] = {}
751+
for s in sorted(slots, key=lambda s: (id_space_order[s.id_space], s.idx)):
752+
key = (s.id_space, s.idx)
753+
gid = key_to_global.get(key)
754+
if gid is None:
755+
gid = len(key_to_global)
756+
key_to_global[key] = gid
757+
mapping[s] = gid
758+
return mapping
759+
722760
# Create Tid mapping
723-
slot_to_tid = sorted(
724-
[s for s in used_slots if s.id_type == IdType.Tensor],
725-
key=lambda s: (id_space_order[s.id_space], s.idx),
726-
)
727-
slot_to_tid = {s: idx for idx, s in enumerate(slot_to_tid)}
761+
slot_to_tid = _coalesce([s for s in used_slots if s.id_type == IdType.Tensor])
728762

729763
# Create Vid mapping
730-
slot_to_vid = sorted(
731-
[s for s in used_slots if s.id_type != IdType.Tensor],
732-
key=lambda s: (id_space_order[s.id_space], s.idx),
733-
)
734-
slot_to_vid = {s: idx for idx, s in enumerate(slot_to_vid)}
764+
slot_to_vid = _coalesce([s for s in used_slots if s.id_type != IdType.Tensor])
735765

736766
# Remap all Tid/Vid values in instructions to use global indices
737767
if hasattr(self, "_tid_slot_map"):

backends/mlx/builder/slot_manager.py

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
import uuid
1010
from collections import defaultdict
11+
from contextlib import contextmanager
1112
from dataclasses import dataclass
1213
from enum import auto, Enum
13-
from typing import Dict, Optional, Tuple, Union
14+
from typing import Dict, Iterator, List, Optional, Tuple, Union
1415

1516
import torch
1617
from torch.fx.node import Node
@@ -73,10 +74,72 @@ def __init__(self):
7374
self.tid_managers: Dict[IdSpace, IdManager] = defaultdict(IdManager)
7475
self.vid_managers: Dict[IdSpace, IdManager] = defaultdict(IdManager)
7576
self.name_to_slot: Dict[str, Slot] = {}
77+
# Stack of active temp-slot scopes (see ``tmp_scope``). Temp tids/vids
78+
# allocated via make_tmp_slot()/make_tmp_value_slot() are registered on
79+
# the innermost scope and their ids returned for reuse on scope exit.
80+
self._tmp_scopes: List[List[Slot]] = []
81+
82+
@contextmanager
83+
def tmp_scope(self) -> Iterator[None]:
84+
"""Scope temporary slot allocations so their ids can be reused.
85+
86+
Temp tids/vids allocated via :meth:`make_tmp_slot` /
87+
:meth:`make_tmp_value_slot` inside this context are returned to their
88+
id pools when the context exits, so later allocations (temp or node)
89+
can reuse them. Allocating a temp slot outside any ``tmp_scope`` raises
90+
``RuntimeError``.
91+
92+
Scopes may be nested; each allocation is tied to the innermost scope.
93+
The Slot objects stay in ``name_to_slot`` (mirroring node-slot reclaim
94+
via ``return_id``) so serialization still sees every distinct slot.
95+
96+
Invariant: the slots yielded here are dead after the context exits.
97+
Never ``set_slot`` a temp slot as a node's persistent output because its id is
98+
reclaimed on scope exit and would be reused (and coalesced) by a later
99+
node while still live. Node outputs must come from ``make_or_get_slot``.
100+
"""
101+
self._tmp_scopes.append([])
102+
try:
103+
yield
104+
finally:
105+
scope = self._tmp_scopes.pop()
106+
for slot in scope:
107+
if slot.id_type == IdType.Tensor:
108+
self.tid_managers[slot.id_space].return_id(slot.idx)
109+
else:
110+
self.vid_managers[slot.id_space].return_id(slot.idx)
111+
112+
def _new_tmp_slot(self, id_type: IdType, prefix: str) -> Tuple[str, Slot]:
113+
if not self._tmp_scopes:
114+
raise RuntimeError(
115+
f"{prefix}() must be called within a SlotManager.tmp_scope() "
116+
"context so temporary ids can be reclaimed and reused."
117+
)
118+
name = f"{prefix}_{uuid.uuid4().hex}"
119+
id_space = IdSpace.Temp
120+
manager = (
121+
self.tid_managers[id_space]
122+
if id_type == IdType.Tensor
123+
else self.vid_managers[id_space]
124+
)
125+
idx = manager.get_id()
126+
slot = Slot(id_type=id_type, id_space=id_space, idx=idx)
127+
self.name_to_slot[name] = slot
128+
self._tmp_scopes[-1].append(slot)
129+
return name, slot
76130

77131
def set_slot(self, node_or_name: Union[Node, str], slot: Slot):
78132
if isinstance(node_or_name, Node):
79133
node_or_name = node_or_name.name
134+
# A slot still tracked by an active tmp_scope has its id reclaimed when the
135+
# scope exits, so it must never be bound as a node's persistent output (a
136+
# later node would read it as dead). Node outputs must come from
137+
# make_or_get_slot(). See SlotManager.tmp_scope().
138+
assert not any(slot in scope for scope in self._tmp_scopes), (
139+
f"Cannot bind temporary slot {slot} as the output of {node_or_name}; "
140+
f"its id is reclaimed on tmp_scope exit. Use make_or_get_slot() for "
141+
f"node outputs."
142+
)
80143
# Allow setting a slot to the same value (e.g., for in-place ops like SLICE_UPDATE)
81144
existing = self.name_to_slot.get(node_or_name)
82145
if existing is not None:
@@ -129,23 +192,11 @@ def make_constant_slot(self, name: str) -> Slot:
129192
return slot
130193

131194
def make_tmp_slot(self) -> Tuple[str, Slot]:
132-
name = f"tmp_{uuid.uuid4().hex}"
133-
id_space = IdSpace.Temp
134-
manager = self.tid_managers[id_space]
135-
idx = manager.get_id()
136-
slot = Slot(id_type=IdType.Tensor, id_space=id_space, idx=idx)
137-
self.name_to_slot[name] = slot
138-
return name, slot
195+
return self._new_tmp_slot(IdType.Tensor, "tmp")
139196

140197
def make_tmp_value_slot(self) -> Tuple[str, Slot]:
141198
"""Create a temporary SymInt slot and register it."""
142-
name = f"tmp_val_{uuid.uuid4().hex}"
143-
id_space = IdSpace.Temp
144-
manager = self.vid_managers[id_space]
145-
idx = manager.get_id()
146-
slot = Slot(id_type=IdType.SymInt, id_space=id_space, idx=idx)
147-
self.name_to_slot[name] = slot
148-
return name, slot
199+
return self._new_tmp_slot(IdType.SymInt, "tmp_val")
149200

150201
def make_or_get_slots(
151202
self, node: Node, id_space: IdSpace = IdSpace.Temp

0 commit comments

Comments
 (0)