Skip to content

Commit 03ee233

Browse files
Skip externalize for submodules not invoked in the exported graph
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.
1 parent 7171b3b commit 03ee233

3 files changed

Lines changed: 177 additions & 43 deletions

File tree

coreai_torch/_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,15 +1575,15 @@ def _find_program_for(
15751575
name: str,
15761576
op_name: str,
15771577
programs: dict[str, ExportedProgram],
1578-
) -> ExportedProgram:
1579-
"""Find the nearest ancestor program that contains *op_name*."""
1578+
) -> ExportedProgram | None:
1579+
"""Find the nearest ancestor program that contains *op_name*, or ``None``."""
15801580
for ancestor in _ancestor_paths(name):
15811581
if (
15821582
ancestor in programs
15831583
and _find_custom_op_node(programs[ancestor], op_name) is not None
15841584
):
15851585
return programs[ancestor]
1586-
raise ValueError(f"Custom op for '{name}' not found in any ancestor program")
1586+
return None
15871587

15881588

15891589
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: 162 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3032,10 +3032,12 @@ def forward(
30323032

30333033
@pytest.mark.ir
30343034
@pytest.mark.flaky(reruns=3)
3035-
@pytest.mark.skip(reason=(
3036-
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3037-
"these tests or use an alternative way to generate quantized weights"
3038-
))
3035+
@pytest.mark.skip(
3036+
reason=(
3037+
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3038+
"these tests or use an alternative way to generate quantized weights"
3039+
)
3040+
)
30393041
def test_externalize_rms_norm_with_quantized_linears_ir() -> None:
30403042
"""IR check: Quantized (int4) weights retain si4 dtype when externalization re-exports the model."""
30413043

@@ -3065,7 +3067,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
30653067
)
30663068
quantizer = PostTrainingQuantizer(model, quantization_config)
30673069
model = cast("nn.Module", quantizer.compress())
3068-
transform_with_custom_compression_ops(model)
3070+
transform_with_custom_compression_ops(model) # noqa: F821
30693071

30703072
sample = (torch.randn(2, 24),)
30713073

@@ -3109,10 +3111,12 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
31093111

31103112

31113113
@pytest.mark.flaky(reruns=3)
3112-
@pytest.mark.skip(reason=(
3113-
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3114-
"these tests or use an alternative way to generate quantized weights"
3115-
))
3114+
@pytest.mark.skip(
3115+
reason=(
3116+
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3117+
"these tests or use an alternative way to generate quantized weights"
3118+
)
3119+
)
31163120
async def test_externalize_rms_norm_with_quantized_linears() -> None:
31173121
"""Quantized (int4) weights retain si4 dtype when externalization re-exports the model.
31183122
@@ -3153,7 +3157,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
31533157
)
31543158
quantizer = PostTrainingQuantizer(model, quantization_config)
31553159
model = cast("nn.Module", quantizer.compress())
3156-
transform_with_custom_compression_ops(model)
3160+
transform_with_custom_compression_ops(model) # noqa: F821
31573161

31583162
sample = (torch.randn(2, 24),)
31593163

@@ -3176,10 +3180,12 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
31763180

31773181
@pytest.mark.ir
31783182
@pytest.mark.flaky(reruns=3)
3179-
@pytest.mark.skip(reason=(
3180-
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3181-
"these tests or use an alternative way to generate quantized weights"
3182-
))
3183+
@pytest.mark.skip(
3184+
reason=(
3185+
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3186+
"these tests or use an alternative way to generate quantized weights"
3187+
)
3188+
)
31833189
def test_externalize_gather_mm_with_quantized_rhs_ir() -> None:
31843190
"""IR check: Quantized expert weight flows as rhs into an externalized GatherMM composite."""
31853191
num_experts = 4
@@ -3217,7 +3223,7 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
32173223
)
32183224
quantizer = PostTrainingQuantizer(model, quantization_config)
32193225
model = cast("nn.Module", quantizer.compress())
3220-
transform_with_custom_compression_ops(model)
3226+
transform_with_custom_compression_ops(model) # noqa: F821
32213227

32223228
x = torch.randn(2, 1, 1, in_dim)
32233229
indices = torch.tensor([[0, 2], [1, 3]], dtype=torch.int16)
@@ -3263,10 +3269,12 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
32633269

32643270

32653271
@pytest.mark.flaky(reruns=3)
3266-
@pytest.mark.skip(reason=(
3267-
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3268-
"these tests or use an alternative way to generate quantized weights"
3269-
))
3272+
@pytest.mark.skip(
3273+
reason=(
3274+
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3275+
"these tests or use an alternative way to generate quantized weights"
3276+
)
3277+
)
32703278
async def test_externalize_gather_mm_with_quantized_rhs() -> None:
32713279
"""Quantized expert weight flows as rhs into an externalized GatherMM composite.
32723280
@@ -3318,7 +3326,7 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
33183326
)
33193327
quantizer = PostTrainingQuantizer(model, quantization_config)
33203328
model = cast("nn.Module", quantizer.compress())
3321-
transform_with_custom_compression_ops(model)
3329+
transform_with_custom_compression_ops(model) # noqa: F821
33223330

33233331
x = torch.randn(2, 1, 1, in_dim)
33243332
indices = torch.tensor([[0, 2], [1, 3]], dtype=torch.int16)
@@ -3345,10 +3353,12 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
33453353

33463354
@pytest.mark.ir
33473355
@pytest.mark.flaky(reruns=3)
3348-
@pytest.mark.skip(reason=(
3349-
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3350-
"these tests or use an alternative way to generate quantized weights"
3351-
))
3356+
@pytest.mark.skip(
3357+
reason=(
3358+
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3359+
"these tests or use an alternative way to generate quantized weights"
3360+
)
3361+
)
33523362
def test_externalize_multiple_composites_with_quantized_weights_ir() -> None:
33533363
"""IR check: Multiple composite ops (RMSNorm + SDPA) externalized with quantized linears."""
33543364
head_dim = 16
@@ -3390,7 +3400,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
33903400
)
33913401
quantizer = PostTrainingQuantizer(model, quantization_config)
33923402
model = cast("nn.Module", quantizer.compress())
3393-
transform_with_custom_compression_ops(model)
3403+
transform_with_custom_compression_ops(model) # noqa: F821
33943404

33953405
sample = (torch.randn(1, 4, embed_dim),)
33963406

@@ -3433,10 +3443,12 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
34333443

34343444

34353445
@pytest.mark.flaky(reruns=3)
3436-
@pytest.mark.skip(reason=(
3437-
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3438-
"these tests or use an alternative way to generate quantized weights"
3439-
))
3446+
@pytest.mark.skip(
3447+
reason=(
3448+
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3449+
"these tests or use an alternative way to generate quantized weights"
3450+
)
3451+
)
34403452
async def test_externalize_multiple_composites_with_quantized_weights() -> None:
34413453
"""Multiple composite ops (RMSNorm + SDPA) externalized with quantized linears.
34423454
@@ -3484,7 +3496,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
34843496
)
34853497
quantizer = PostTrainingQuantizer(model, quantization_config)
34863498
model = cast("nn.Module", quantizer.compress())
3487-
transform_with_custom_compression_ops(model)
3499+
transform_with_custom_compression_ops(model) # noqa: F821
34883500

34893501
sample = (torch.randn(1, 4, embed_dim),)
34903502

@@ -3512,10 +3524,12 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
35123524

35133525

35143526
@pytest.mark.ir
3515-
@pytest.mark.skip(reason=(
3516-
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3517-
"these tests or use an alternative way to generate quantized weights"
3518-
))
3527+
@pytest.mark.skip(
3528+
reason=(
3529+
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3530+
"these tests or use an alternative way to generate quantized weights"
3531+
)
3532+
)
35193533
def test_externalize_gather_mm_combined_with_rms_norm_ir() -> None:
35203534
"""IR check: GatherMM + RMSNorm both externalized alongside quantized weights."""
35213535
num_experts = 4
@@ -3553,7 +3567,7 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
35533567
)
35543568
quantizer = PostTrainingQuantizer(model, quantization_config)
35553569
model = cast("nn.Module", quantizer.compress())
3556-
transform_with_custom_compression_ops(model)
3570+
transform_with_custom_compression_ops(model) # noqa: F821
35573571

35583572
x = torch.randn(2, in_dim)
35593573
indices = torch.tensor([[0, 2], [1, 3]], dtype=torch.int16)
@@ -3590,10 +3604,13 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
35903604
"""
35913605
filecheck_pattern(ir, check_file=pattern)
35923606

3593-
@pytest.mark.skip(reason=(
3594-
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3595-
"these tests or use an alternative way to generate quantized weights"
3596-
))
3607+
3608+
@pytest.mark.skip(
3609+
reason=(
3610+
"transform_with_custom_compression_ops has been deprecated. Consider removing "
3611+
"these tests or use an alternative way to generate quantized weights"
3612+
)
3613+
)
35973614
async def test_externalize_gather_mm_combined_with_rms_norm() -> None:
35983615
"""GatherMM + RMSNorm both externalized alongside quantized weights.
35993616
@@ -3637,7 +3654,7 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
36373654
)
36383655
quantizer = PostTrainingQuantizer(model, quantization_config)
36393656
model = cast("nn.Module", quantizer.compress())
3640-
transform_with_custom_compression_ops(model)
3657+
transform_with_custom_compression_ops(model) # noqa: F821
36413658

36423659
x = torch.randn(2, in_dim)
36433660
indices = torch.tensor([[0, 2], [1, 3]], dtype=torch.int16)
@@ -3666,3 +3683,108 @@ def forward(self, x: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:
36663683
await _validate_numerics(
36673684
coreai_program, model, sample, input_names=("x", "indices")
36683685
)
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)