Skip to content

Commit 012f399

Browse files
authored
fix: raise CoreMLExportError on incompatible palettization configs (#44)
* refactor: add validate_coreml_palettization_compatibility for combo checks Vector palettization, LUT quantization, and per-channel scale are each independently CoreML-exportable, but combining any two breaks CoreML/MIL's constexpr_lut_to_dense op with a rank mismatch, a missing vector_axis, or an indices/LUT divisibility error depending on the pair. Add a dedicated validator (composing the existing LUT dtype check) so the kmeans export path has one entry point to reject these combinations with a clear CoreMLExportError, instead of relying on a test-side skip to hide them. * refactor: reject unsupported palettization combos in kmeans CoreML export Wire validate_coreml_palettization_compatibility into prepare_for_mil_export so finalize(backend=CoreML) raises immediately for a combo it can't fuse into a single compatible op chain, rather than emitting an invalid model or crashing deep inside ct.convert. * test: reject unsupported kmeans palettization combos on CoreML export Replace the pytest.skip-based _skip_unsupported_mil_configs with a predicate (_has_unsupported_mil_combo) and assert finalize() raises CoreMLExportError, mirroring the existing float-LUT-dtype rejection path. Rename _assert_coreml_rejects_unsupported_lut to _assert_coreml_rejects since it's now shared by both rejection cases.
1 parent 3f5b05d commit 012f399

3 files changed

Lines changed: 82 additions & 43 deletions

File tree

src/coreai_opt/_utils/export_utils.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from coreai_opt._utils.torch_utils import is_tensor_on_cpu
1212
from coreai_opt.common import CoreMLExportError, ExportBackend
1313
from coreai_opt.config.spec import CompressionTargetTensor
14+
from coreai_opt.quantization.spec import QuantizationSpec
1415
from coreai_opt.quantization.spec.granularity import PerTensorGranularity, QuantizationGranularity
1516

1617
COREML_SUPPORTED_WEIGHT_DTYPES: frozenset[torch.dtype] = frozenset(
@@ -78,6 +79,52 @@ def validate_coreml_compatibility(
7879
raise CoreMLExportError.from_config(granularity, context)
7980

8081

82+
def validate_coreml_palettization_compatibility(
83+
cluster_dim: int,
84+
lut_qspec: QuantizationSpec | None,
85+
enable_per_channel_scale: bool,
86+
context: str,
87+
) -> None:
88+
"""Raise CoreMLExportError if this palettization config isn't CoreML-exportable.
89+
90+
Checks the LUT dtype (delegating to validate_coreml_compatibility) and
91+
whether the config combines multiple features CoreML/MIL export cannot yet
92+
fuse into a single compatible op chain: CoreML export supports at most one
93+
of {vector palettization, LUT quantization, per-channel scale} at a time;
94+
combining any two hits an unsupported CoreML/MIL op configuration
95+
(mismatched tensor ranks, or `lut_to_dense` divisibility errors).
96+
97+
Args:
98+
cluster_dim (int): Palettization cluster dimension; > 1 indicates
99+
vector palettization.
100+
lut_qspec (QuantizationSpec | None): LUT quantization spec, or None if
101+
the LUT is not quantized.
102+
enable_per_channel_scale (bool): Whether per-channel scaling is enabled.
103+
context (str): Human-readable description of what's being checked.
104+
105+
Raises:
106+
CoreMLExportError: If the LUT dtype isn't supported, or if two or more
107+
of the three features above are combined.
108+
"""
109+
if lut_qspec is not None:
110+
validate_coreml_compatibility(
111+
CompressionTargetTensor.LUT, lut_qspec.dtype, f"LUT of {context}"
112+
)
113+
114+
active_features = []
115+
if cluster_dim > 1:
116+
active_features.append("cluster_dim")
117+
if lut_qspec is not None:
118+
active_features.append("lut_qspec")
119+
if enable_per_channel_scale:
120+
active_features.append("enable_per_channel_scale")
121+
122+
if len(active_features) >= 2:
123+
raise CoreMLExportError(
124+
f"CoreML export does not support {' + '.join(active_features)} on {context}."
125+
)
126+
127+
81128
def validate_mmap_backend_and_device(
82129
model: torch.nn.Module,
83130
backend: ExportBackend,

src/coreai_opt/palettization/kmeans/_prepare_for_export.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,14 @@
1414
from coreai_opt._utils.export_utils import (
1515
clear_parametrization_original as _clear_parametrization_original,
1616
prepare_mmap_dir as _prepare_mmap_dir,
17-
validate_coreml_compatibility,
17+
validate_coreml_palettization_compatibility,
1818
)
1919
from coreai_opt._utils.import_utils import lazy_import_coreai_torch
2020
from coreai_opt._utils.metadata_utils import CompressionType, MILCompressionMetadata
2121
from coreai_opt._utils.torch_utils import (
2222
mmap_module_state_dict as _mmap_module_state_dict,
2323
)
2424
from coreai_opt.common import ExportBackend
25-
from coreai_opt.config.spec import CompressionTargetTensor
2625
from coreai_opt.palettization.spec.fake_palettize import (
2726
_FakePalettizeImplBase,
2827
)
@@ -430,12 +429,16 @@ def prepare_for_mil_export(model: nn.Module) -> nn.Module:
430429
continue
431430
for param_name, parametrizations in module.parametrizations.items():
432431
_, fake_palett_mod = _find_fake_palett_parametrization(parametrizations)
433-
if fake_palett_mod is not None and fake_palett_mod.lut_qspec is not None:
434-
validate_coreml_compatibility(
435-
CompressionTargetTensor.LUT,
436-
fake_palett_mod.lut_qspec.dtype,
437-
f"LUT of parameter '{param_name}' of module '{module_name}'",
438-
)
432+
if fake_palett_mod is None:
433+
continue
434+
435+
context = f"parameter '{param_name}' of module '{module_name}'"
436+
validate_coreml_palettization_compatibility(
437+
fake_palett_mod.cluster_dim,
438+
fake_palett_mod.lut_qspec,
439+
fake_palett_mod.enable_per_channel_scale,
440+
context,
441+
)
439442

440443
_process_weight_palettization(model, backend=ExportBackend.CoreML)
441444

tests/export/test_kmeans_export.py

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,17 @@ def _has_float_lut(config: ParametrizedPalettConfigs) -> bool:
6363
return config.lut_qspec is not None and config.lut_qspec.dtype.is_floating_point
6464

6565

66-
def _assert_coreml_rejects_unsupported_lut(
66+
def _assert_coreml_rejects(
6767
model: torch.nn.Module,
6868
input_data: torch.Tensor,
6969
config: KMeansPalettizerConfig,
7070
) -> None:
71-
"""Assert finalize(CoreML) rejects an unsupported LUT dtype.
71+
"""Assert finalize(CoreML) rejects an unsupported palettization config.
7272
73-
CoreML/MIL does not support FP or INT2 LUT quantization, so finalize must raise
74-
rather than emit an invalid model.
73+
CoreML/MIL rejects certain LUT dtypes and certain combinations of
74+
palettization features (vector palettization, LUT quantization,
75+
per-channel scale), so finalize must raise rather than emit an invalid
76+
model.
7577
"""
7678
model.eval()
7779
palettizer = KMeansPalettizer(model, config)
@@ -95,32 +97,17 @@ def _skip_heavy_mnist_configs(config: ParametrizedPalettConfigs) -> None:
9597
pytest.skip(f"MNIST only tests lut_qspec with int8 and float8_e4m3fn, got {dtype}")
9698

9799

98-
def _skip_unsupported_mil_configs(
99-
backend: ExportBackend,
100-
config: ParametrizedPalettConfigs,
101-
) -> None:
102-
"""Skip CoreML configs with unsupported feature combinations."""
103-
if backend != ExportBackend.CoreML:
104-
return
105-
100+
def _has_unsupported_mil_combo(config: ParametrizedPalettConfigs) -> bool:
101+
"""Whether the config combines >=2 of {vector palettization, LUT
102+
quantization, per-channel scale} -- verified to be the exact set
103+
CoreML/MIL cannot export: any single one of these works in isolation, but
104+
combining two or more fails with a rank-mismatch, missing-vector_axis, or
105+
LUT-divisibility error depending on the pair.
106+
"""
106107
is_vector = config.cluster_dim > 1
107108
has_lut_quant = config.lut_qspec is not None
108109
has_pcs = config.enable_per_channel_scale
109-
110-
# Vector palettization + LUT quantization
111-
if is_vector and has_lut_quant:
112-
# TODO: add CoreML export support for palettization combos.
113-
pytest.skip("CoreML export not supported for vector palettization + LUT quantization.")
114-
115-
# Vector palettization + per-channel scale
116-
if is_vector and has_pcs:
117-
# TODO: add CoreML export support for palettization combos.
118-
pytest.skip("CoreML export not supported for vector palettization + per-channel scale.")
119-
120-
# LUT quantization + per-channel scale
121-
if has_lut_quant and has_pcs:
122-
# TODO: add CoreML export support for palettization combos.
123-
pytest.skip("CoreML export not supported for LUT quantization + per-channel scale.")
110+
return sum([is_vector, has_lut_quant, has_pcs]) >= 2
124111

125112

126113
@pytest.mark.parametrize("backend", [ExportBackend.CoreML, ExportBackend.CoreAI])
@@ -134,12 +121,13 @@ def test_simple_model_export(
134121
config = parametrized_palett_config.config
135122
granularity = parametrized_palett_config.granularity
136123

137-
if backend == ExportBackend.CoreML and _has_float_lut(parametrized_palett_config):
138-
_assert_coreml_rejects_unsupported_lut(simple_conv_linear_model, simple_model_input, config)
124+
if backend == ExportBackend.CoreML and (
125+
_has_float_lut(parametrized_palett_config)
126+
or _has_unsupported_mil_combo(parametrized_palett_config)
127+
):
128+
_assert_coreml_rejects(simple_conv_linear_model, simple_model_input, config)
139129
return
140130

141-
_skip_unsupported_mil_configs(backend, parametrized_palett_config)
142-
143131
if (
144132
backend == ExportBackend.CoreML
145133
and parametrized_palett_config.cluster_dim > 1
@@ -175,12 +163,13 @@ def test_mnist_export(
175163
config = parametrized_palett_config.config
176164
granularity = parametrized_palett_config.granularity
177165

178-
if backend == ExportBackend.CoreML and _has_float_lut(parametrized_palett_config):
179-
_assert_coreml_rejects_unsupported_lut(custom_test_mnist_model, mnist_example_input, config)
166+
if backend == ExportBackend.CoreML and (
167+
_has_float_lut(parametrized_palett_config)
168+
or _has_unsupported_mil_combo(parametrized_palett_config)
169+
):
170+
_assert_coreml_rejects(custom_test_mnist_model, mnist_example_input, config)
180171
return
181172

182-
_skip_unsupported_mil_configs(backend, parametrized_palett_config)
183-
184173
# The MNIST model has 6 weight-bearing layers (conv1, conv2, conv_transpose1,
185174
# conv_transpose2, dense1, dense2). For axis=1 with group_size=2, conv1's
186175
# axis-1 (in_channels=1) is not divisible, so palettization is skipped there.

0 commit comments

Comments
 (0)