Skip to content

Commit ea728d6

Browse files
Skip externalize for submodules not invoked in the exported graph (#18)
When a model registers a submodule whose class matches an externalize_modules target but the model's forward never calls it, the re-exported FX graph contains no custom-op node for that submodule. The pipeline previously raised ``ValueError: Custom op for '<name>' not found in any ancestor program`` from _find_program_for, aborting the whole conversion. Skip these submodules (with a UserWarning) instead. The unused module's forward is still restored by _restore_externalized. Also silence pre-existing F821 lint warnings on transform_with_custom_compression_ops references in skipped tests so pre-commit ruff-check passes. Co-authored-by: gokulkrishna98 <gokulkrishna98@users.noreply.github.com>
1 parent a68f1ad commit ea728d6

3 files changed

Lines changed: 120 additions & 3 deletions

File tree

coreai_torch/_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,15 +1596,15 @@ def _find_program_for(
15961596
name: str,
15971597
op_name: str,
15981598
programs: dict[str, ExportedProgram],
1599-
) -> ExportedProgram:
1600-
"""Find the nearest ancestor program that contains *op_name*."""
1599+
) -> ExportedProgram | None:
1600+
"""Find the nearest ancestor program that contains *op_name*, or ``None``."""
16011601
for ancestor in _ancestor_paths(name):
16021602
if (
16031603
ancestor in programs
16041604
and _find_custom_op_node(programs[ancestor], op_name) is not None
16051605
):
16061606
return programs[ancestor]
1607-
raise ValueError(f"Custom op for '{name}' not found in any ancestor program")
1607+
return None
16081608

16091609

16101610
def _reverse_lookup(mapping: dict[str, str], value: str) -> str | None:

coreai_torch/externalize.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,18 @@ def __iter__(self) -> Iterator[_PreparedModule]:
431431
name: str = mod._externalize_name # type: ignore[attr-defined]
432432
op_name: str = mod._externalize_op_name # type: ignore[attr-defined]
433433
parent_ep = _find_program_for(name, op_name, self._programs)
434+
if parent_ep is None:
435+
# Marked submodule is not invoked in the exported graph.
436+
# Skip it; its forward will be restored by _restore_externalized.
437+
warnings.warn(
438+
f"\n[WARN] coreai_torch.externalize: skipping unused submodule '{name}'.\n"
439+
f" It matched an externalize_modules target class but is not "
440+
f"reachable from the exported graph.\n"
441+
f" Action: remove it from the model passed to add_pytorch_module, "
442+
f"or ignore if intentional.\n",
443+
stacklevel=2,
444+
)
445+
continue
434446
preps = _prepare_module_export(mod, parent_ep)
435447
for prep in preps:
436448
prep._program_registry = self

tests/test_externalize.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3683,3 +3683,108 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
36833683
await _validate_numerics(
36843684
coreai_program, model, sample, input_names=("x", "indices")
36853685
)
3686+
3687+
3688+
@pytest.mark.ir
3689+
def test_externalize_unused_submodule_ir() -> None:
3690+
"""An externalizable submodule that the model's forward never calls is skipped.
3691+
3692+
Previously, a registered submodule that did not appear in the exported graph
3693+
caused the externalize pipeline to raise ``ValueError: Custom op for ...
3694+
not found in any ancestor program``. The pipeline should instead warn and
3695+
proceed, lowering the rest of the model normally.
3696+
"""
3697+
3698+
class InnerModule(nn.Module):
3699+
def __init__(self):
3700+
super().__init__()
3701+
self.fc = nn.Linear(4, 4)
3702+
3703+
def forward(self, x: torch.Tensor) -> torch.Tensor:
3704+
return torch.relu(self.fc(x))
3705+
3706+
class OuterModel(nn.Module):
3707+
def __init__(self):
3708+
super().__init__()
3709+
self.pre = nn.Linear(4, 4)
3710+
# Registered as a submodule but intentionally never invoked.
3711+
self.unused = InnerModule()
3712+
self.post = nn.Linear(4, 4)
3713+
3714+
def forward(self, x: torch.Tensor) -> torch.Tensor:
3715+
return self.post(self.pre(x))
3716+
3717+
torch.manual_seed(42)
3718+
model = OuterModel().eval()
3719+
sample = (torch.randn(2, 4),)
3720+
3721+
with pytest.warns(UserWarning, match="skipping unused submodule"):
3722+
converter = TorchConverter().add_pytorch_module(
3723+
model,
3724+
export_fn=lambda m: torch.export.export(m, args=sample).run_decompositions(
3725+
get_decomp_table()
3726+
),
3727+
externalize_modules=[InnerModule],
3728+
)
3729+
coreai_program = converter.to_coreai()
3730+
3731+
check_file = """
3732+
// CHECK-LABEL: module {
3733+
// CHECK-NOT: coreai.graph noinline
3734+
// CHECK: coreai.graph @main(
3735+
// CHECK-SAME: %[[ARG0:[a-zA-Z0-9_]+]]: tensor<2x4xf32> {coreai.name = "x"}
3736+
// CHECK-SAME: ) -> (tensor<2x4xf32>
3737+
// CHECK: %[[MM0:[0-9a-z_]+]] = coreai.decomposable.broadcasting_batch_matmul %[[ARG0]],
3738+
// CHECK-SAME: : (tensor<2x4xf32>, tensor<4x4xf32>) -> tensor<2x4xf32>
3739+
// CHECK: %[[ADD0:[0-9a-z_]+]] = coreai.decomposable.broadcasting_add %[[MM0]],
3740+
// CHECK-SAME: : (tensor<2x4xf32>, tensor<4xf32>) -> tensor<2x4xf32>
3741+
// CHECK: %[[MM1:[0-9a-z_]+]] = coreai.decomposable.broadcasting_batch_matmul %[[ADD0]],
3742+
// CHECK-SAME: : (tensor<2x4xf32>, tensor<4x4xf32>) -> tensor<2x4xf32>
3743+
// CHECK: %[[ADD1:[0-9a-z_]+]] = coreai.decomposable.broadcasting_add %[[MM1]],
3744+
// CHECK-SAME: : (tensor<2x4xf32>, tensor<4xf32>) -> tensor<2x4xf32>
3745+
// CHECK-NOT: coreai.invoke
3746+
// CHECK-NOT: coreai.relu
3747+
// CHECK: coreai.output %[[ADD1]] : tensor<2x4xf32>
3748+
// CHECK: }
3749+
// CHECK-NOT: coreai.graph
3750+
// CHECK: }
3751+
"""
3752+
filecheck_pattern(str(coreai_program), check_file=check_file)
3753+
3754+
3755+
async def test_externalize_unused_submodule_numerics() -> None:
3756+
"""Numerics: unused externalizable submodule does not affect output."""
3757+
3758+
class InnerModule(nn.Module):
3759+
def __init__(self):
3760+
super().__init__()
3761+
self.fc = nn.Linear(4, 4)
3762+
3763+
def forward(self, x: torch.Tensor) -> torch.Tensor:
3764+
return torch.relu(self.fc(x))
3765+
3766+
class OuterModel(nn.Module):
3767+
def __init__(self):
3768+
super().__init__()
3769+
self.pre = nn.Linear(4, 4)
3770+
self.unused = InnerModule()
3771+
self.post = nn.Linear(4, 4)
3772+
3773+
def forward(self, x: torch.Tensor) -> torch.Tensor:
3774+
return self.post(self.pre(x))
3775+
3776+
torch.manual_seed(42)
3777+
model = OuterModel().eval()
3778+
sample = (torch.randn(2, 4),)
3779+
3780+
with pytest.warns(UserWarning, match="skipping unused submodule"):
3781+
converter = TorchConverter().add_pytorch_module(
3782+
model,
3783+
export_fn=lambda m: torch.export.export(m, args=sample).run_decompositions(
3784+
get_decomp_table()
3785+
),
3786+
externalize_modules=[InnerModule],
3787+
)
3788+
coreai_program = converter.to_coreai()
3789+
3790+
await _validate_numerics(coreai_program, model, sample)

0 commit comments

Comments
 (0)