Skip to content

Commit 336aaac

Browse files
authored
Merge branch 'apple:main' into create-env-lowest-torch-venv
2 parents 3fa8e49 + 6513455 commit 336aaac

16 files changed

Lines changed: 410 additions & 233 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,23 @@ jobs:
3838
run: make check
3939

4040
test-smoke:
41-
name: Linux / make test-smoke
41+
name: Linux / make test-smoke / torch=${{ matrix.torch_version }}
4242
runs-on: ubuntu-latest
4343
timeout-minutes: 60
4444
env:
4545
INSTALL_PRECOMMIT: "false"
46+
strategy:
47+
fail-fast: false
48+
matrix:
49+
include:
50+
- torch_group: torch_2_8
51+
torch_version: "2.8"
52+
- torch_group: torch_2_9
53+
torch_version: "2.9"
54+
- torch_group: torch_2_10
55+
torch_version: "2.10"
56+
- torch_group: torch_2_11
57+
torch_version: "2.11"
4658
steps:
4759
- name: Check out repository
4860
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -51,7 +63,7 @@ jobs:
5163
with:
5264
enable-cache: false
5365
- name: Run `make test-smoke`
54-
run: make test-smoke
66+
run: make test-smoke TORCH_GROUP=${{ matrix.torch_group }}
5567

5668
test-tutorials:
5769
name: Linux / make test-tutorials

Makefile

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ VENV_HIGHEST_TORCH ?= .venv-highest-torch
5353
VENV_LOWEST_TORCH ?= .venv-lowest-torch
5454
VENV_TUTORIAL ?= .venv-tutorial
5555

56+
# The torch_2_* groups (pyproject.toml [dependency-groups]) currently at
57+
# each end of the supported range. Bump these two lines — nothing else —
58+
# when the project's torch version bounds change.
59+
HIGHEST_TORCH_GROUP := torch_2_11
60+
LOWEST_TORCH_GROUP := torch_2_8
61+
62+
# Torch dependency group (pyproject.toml [dependency-groups]) that every
63+
# environment-building target (env, test, test-smoke, docs, ...) pins to.
64+
TORCH_GROUP ?= $(HIGHEST_TORCH_GROUP)
65+
export TORCH_GROUP
66+
5667
# Documentation directory. Defaults to $(MAKEFILE_DIR)docs so the same recipe
5768
# works in both contexts:
5869
#
@@ -176,7 +187,7 @@ env: _maybe_patch_pyproject
176187

177188
# Set up development environment with latest supported PyTorch version
178189
env-highest-torch: _maybe_patch_pyproject
179-
@$(SETUP_ENV) --venv $(VENV_HIGHEST_TORCH) --python-version $(PYTHON_VERSION) --with-highest_tested_torch
190+
@TORCH_GROUP=$(HIGHEST_TORCH_GROUP) $(SETUP_ENV) --venv $(VENV_HIGHEST_TORCH) --python-version $(PYTHON_VERSION)
180191
@$(call write_active_venv,$(VENV_HIGHEST_TORCH))
181192

182193
# Set up development environment with lowest supported PyTorch version
@@ -240,6 +251,7 @@ test-slow:
240251
@$(MAKE) test PYTEST_ARGS="--marker slow"
241252

242253
# Run smoke tests only (pass PYTEST_ARGS for custom flags, e.g., make test-smoke PYTEST_ARGS="--junitxml=results.xml").
254+
# Pass TORCH_GROUP to smoke test against a specific torch version (default: HIGHEST_TORCH_GROUP).
243255
test-smoke:
244256
@$(call use_env,VENV) && \
245257
echo "Running smoke tests..." && \
@@ -249,20 +261,20 @@ test-smoke:
249261
# Run tests on lowest supported PyTorch version (pass PYTEST_ARGS for custom flags)
250262
test-lowest-pytorch:
251263
@echo "Running tests on lowest PyTorch version supported..."
252-
@$(call use_env,VENV_LOWEST_TORCH,--with-lowest_tested_torch) && \
253-
echo "Testing with lowest supported PyTorch versions" && \
254-
uv run --no-sync --active python $(SCRIPTS)/make/log_versions.py && \
255-
$(RUN_TESTS) $(PYTEST_ARGS) && \
256-
echo "All tests passed!"
264+
@TORCH_GROUP=$(LOWEST_TORCH_GROUP) $(call use_env,VENV_LOWEST_TORCH) && \
265+
echo "Testing with lowest supported PyTorch versions" && \
266+
uv run --no-sync --active python $(SCRIPTS)/make/log_versions.py && \
267+
$(RUN_TESTS) $(PYTEST_ARGS) && \
268+
echo "All tests passed!"
257269

258270
# Run tests on highest supported PyTorch version (pass PYTEST_ARGS for custom flags)
259271
test-highest-pytorch:
260272
@echo "Running tests on highest PyTorch version supported..."
261-
@$(call use_env,VENV_HIGHEST_TORCH,--with-highest_tested_torch) && \
262-
echo "Testing with latest supported PyTorch versions" && \
263-
uv run --no-sync --active python $(SCRIPTS)/make/log_versions.py && \
264-
$(RUN_TESTS) $(PYTEST_ARGS) && \
265-
echo "All tests passed!"
273+
@TORCH_GROUP=$(HIGHEST_TORCH_GROUP) $(call use_env,VENV_HIGHEST_TORCH) && \
274+
echo "Testing with latest supported PyTorch versions" && \
275+
uv run --no-sync --active python $(SCRIPTS)/make/log_versions.py && \
276+
$(RUN_TESTS) $(PYTEST_ARGS) && \
277+
echo "All tests passed!"
266278

267279
# Run tutorial notebook tests
268280
test-tutorials:

changelog.d/42.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reject per-channel activation quantization on CoreML export

ci/nox/noxfile.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@
3434
options.default_venv_backend = "uv"
3535
options.error_on_missing_interpreters = True
3636

37+
TORCH_GROUP = os.environ.get("TORCH_GROUP")
3738

38-
@session(python=get_supported_python_versions(), uv_extras=["coreai"], uv_groups=["test"])
39+
40+
@session(
41+
python=get_supported_python_versions(),
42+
uv_extras=["coreai"],
43+
uv_groups=["test", TORCH_GROUP],
44+
)
3945
def smoke_tests(session: Session) -> None:
4046
"""Smoke test the package build and coreai_opt imports and basic functionality.
4147

pyproject.toml

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ dependencies = [
4646
# based on other package versions. We can either 1) add a stricter check to require the newer torchao
4747
# version for everyone, or 2) add a runtime check in src/coreai_opt/init.py to ensure torchao
4848
# version >= 0.15.0 for torch version >= 2.9.0. Opting option 1.
49-
# These torch versions must be in bounds of lowest_tested_torch and highest_tested_torch
49+
# These torch versions must be in bounds of torch_2_8, torch_2_9, torch_2_10, and torch_2_11
5050
"torch>=2.8.0,<=2.11.0",
5151
"torchao>=0.15.0,<=0.17.0",
5252
"tqdm>=4.65",
@@ -107,20 +107,6 @@ docs = [
107107
# This applies to the `coreml` group as well.
108108
coreai = [ "coreai-opt[coreai]" ]
109109
coreml = [ "coreai-opt[coreml]" ]
110-
# Used in CI to force latest mimimum supported torch version
111-
# These torch versions must be in bounds of torch versions listed in project dependencies
112-
highest_tested_torch = [
113-
"torch==2.11.0",
114-
"torchao==0.17.0",
115-
"torchvision==0.26.0",
116-
]
117-
# Used in CI to force lowest mimimum supported torch version
118-
# These torch versions must be in bounds of torch versions listed in project dependencies
119-
lowest_tested_torch = [
120-
"torch==2.8.0",
121-
"torchao==0.15.0",
122-
"torchvision==0.23.0",
123-
]
124110
pre-commit = [
125111
"bashate>=2.1.1",
126112
"darker>=3.0.0",
@@ -144,6 +130,27 @@ pre-commit = [
144130
"tomli-w>=1.0.0",
145131
]
146132
tamm-export = []
133+
# These torch versions must be in bounds of torch versions listed in project dependencies
134+
torch_2_10 = [
135+
"torch==2.10.0",
136+
"torchao==0.16.0",
137+
"torchvision==0.25.0",
138+
]
139+
torch_2_11 = [
140+
"torch==2.11.0",
141+
"torchao==0.17.0",
142+
"torchvision==0.26.0",
143+
]
144+
torch_2_8 = [
145+
"torch==2.8.0",
146+
"torchao==0.15.0",
147+
"torchvision==0.23.0",
148+
]
149+
torch_2_9 = [
150+
"torch==2.9.1",
151+
"torchao==0.15.0",
152+
"torchvision==0.24.1",
153+
]
147154
# Since torch and torchao are project dependencies, we need to include torchvision in dev
148155
# This allows standard `make test` to find torchvision
149156
torchvision = [
@@ -190,8 +197,10 @@ exclude-newer-package = { coreai-core = false, coreai-torch = false }
190197
# Declare conflicting groups so uv does not error
191198
conflicts = [
192199
[
193-
{ group = "highest_tested_torch" },
194-
{ group = "lowest_tested_torch" },
200+
{ group = "torch_2_8" },
201+
{ group = "torch_2_9" },
202+
{ group = "torch_2_10" },
203+
{ group = "torch_2_11" },
195204
],
196205
]
197206
[tool.uv.sources]

scripts/make/setup_env.sh

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ fi
5959
# - in_section && /^\[/: If we're in the section AND hit another section header (line starting with [)
6060
# * exit: Stop processing (we've left the dependency-groups section)
6161
# - in_section {print}: If we're in the section, print the line
62-
# 2. grep -E '^[a-z_-]+ = \[': Filter to lines that define groups
63-
# - ^[a-z_-]+: Group name at start of line (lowercase letters, hyphens, underscores)
62+
# 2. grep -E '^[a-z0-9_-]+ = \[': Filter to lines that define groups
63+
# - ^[a-z0-9_-]+: Group name at start of line (lowercase letters, digits, hyphens, underscores)
6464
# - = \[: Followed by space, equals sign, space, opening bracket
6565
# 3. cut -d' ' -f1: Extract just the group name
6666
# - -d' ': Use space as delimiter
@@ -75,7 +75,7 @@ AVAILABLE_GROUPS=$(
7575
in_section && /^\[/ { exit }
7676
in_section { print }
7777
' "$PYPROJECT_TOML" |
78-
grep -E '^[a-z_-]+ = \[' |
78+
grep -E '^[a-z0-9_-]+ = \[' |
7979
cut -d' ' -f1 |
8080
tr '\n' ' '
8181
)
@@ -109,7 +109,7 @@ ENSURE_MODE=false
109109

110110
# Groups excluded from --all-groups due to mutual conflicts in pyproject.toml.
111111
# tamm-export is omitted because it's opt-in only (never in default-groups or --all-groups).
112-
CONFLICTING_GROUPS=("highest_tested_torch" "lowest_tested_torch")
112+
CONFLICTING_GROUPS=("torch_2_8" "torch_2_9" "torch_2_10" "torch_2_11")
113113

114114
show_help() {
115115
echo "Usage: $0 [OPTIONS]"
@@ -127,6 +127,7 @@ show_help() {
127127
echo ""
128128
echo "Environment variables:"
129129
echo " VENV Virtual environment name, overrides --venv (default: .venv)"
130+
echo " TORCH_GROUP Torch dependency group to pin; source of truth for torch pinning"
130131
echo ""
131132
echo "Available dependency groups: $AVAILABLE_GROUPS"
132133
echo "Conflicting groups (excluded from --all-groups): ${CONFLICTING_GROUPS[*]}"
@@ -135,8 +136,8 @@ show_help() {
135136
echo " $0 --python-version 3.11 # Setup with dev group only"
136137
echo " $0 --python-version 3.11 --with-docs # Setup with dev and docs groups"
137138
echo " $0 --python-version 3.11 --all-groups # Setup with all non-conflicting groups"
138-
echo " $0 --python-version 3.11 --all-groups --with-highest_tested_torch # Setup with all groups and highest torch"
139-
echo " $0 --python-version 3.11 --all-groups --with-lowest_tested_torch # Setup with all groups and lowest torch"
139+
echo " TORCH_GROUP=torch_2_11 $0 --python-version 3.11 --all-groups # Setup with all groups and torch 2.11"
140+
echo " TORCH_GROUP=torch_2_8 $0 --python-version 3.11 --all-groups # Setup with all groups and torch 2.8"
140141
echo " $0 --python-version 3.11 --venv .venv-exp # Setup with custom venv name"
141142
echo " $0 --python-version 3.11 --with-docs --venv .venv-exp # Setup with docs group and custom venv name"
142143
echo " $0 --python-version 3.12 # Setup with Python 3.12"
@@ -222,6 +223,7 @@ validate_groups() {
222223
# Validate dependency group names early
223224
[[ ${#EXTRA_GROUPS[@]} -gt 0 ]] && validate_groups "${EXTRA_GROUPS[@]}"
224225
[[ ${#EXCLUDE_GROUPS[@]} -gt 0 ]] && validate_groups "${EXCLUDE_GROUPS[@]}"
226+
[[ -n "${TORCH_GROUP:-}" ]] && validate_groups "$TORCH_GROUP"
225227

226228
# Check for conflicts between --with-<group> and --without-<group>
227229
if [[ ${#EXTRA_GROUPS[@]} -gt 0 && ${#EXCLUDE_GROUPS[@]} -gt 0 ]]; then
@@ -267,24 +269,35 @@ if [[ "$ENSURE_MODE" == "true" ]] && [ -f "$VENV/bin/python" ]; then
267269
for GROUP in "${EXTRA_GROUPS[@]}"; do
268270
case "$GROUP" in
269271
docs) IMPORT_STMTS+="; import sphinx" ;;
270-
highest_tested_torch | lowest_tested_torch)
271-
IMPORT_STMTS+="; import torchao"
272-
EXPECTED_TORCH="$(group_torch_pin "$GROUP")"
273-
# These groups always pin torch, so an empty result means the
274-
# pyproject parse regressed — fail loudly instead of silently
275-
# skipping the version check (which would reintroduce the bug).
276-
if [[ -z "$EXPECTED_TORCH" ]]; then
277-
echo "Error: could not parse a torch pin for group '$GROUP' in $PYPROJECT_TOML" >&2
278-
exit 1
279-
fi
280-
IMPORT_STMTS+="; import torch; assert torch.__version__.split('+')[0] == '$EXPECTED_TORCH'"
281-
;;
282272
rio) IMPORT_STMTS+="; import turi_lightning" ;;
283273
tamm-export) IMPORT_STMTS+="; import tamm_export" ;;
284274
esac
285275
done
286276
fi
287277

278+
# TORCH_GROUP is the single source of truth for torch pinning; fall back
279+
# to a --with-torch_2_* flag for direct, non-Make invocations. Checked
280+
# unconditionally rather than only when present in EXTRA_GROUPS, since
281+
# TORCH_GROUP drives the sync regardless of what's in EXTRA_GROUPS.
282+
EXPECTED_GROUP="${TORCH_GROUP:-}"
283+
if [[ -z "$EXPECTED_GROUP" ]]; then
284+
for GROUP in "${EXTRA_GROUPS[@]:-}"; do
285+
[[ " ${CONFLICTING_GROUPS[*]} " == *" ${GROUP} "* ]] && EXPECTED_GROUP="$GROUP"
286+
done
287+
fi
288+
if [[ -n "$EXPECTED_GROUP" ]]; then
289+
IMPORT_STMTS+="; import torchao"
290+
EXPECTED_TORCH="$(group_torch_pin "$EXPECTED_GROUP")"
291+
# These groups always pin torch, so an empty result means the
292+
# pyproject parse regressed — fail loudly instead of silently
293+
# skipping the version check (which would reintroduce the bug).
294+
if [[ -z "$EXPECTED_TORCH" ]]; then
295+
echo "Error: could not parse a torch pin for '$EXPECTED_GROUP' in $PYPROJECT_TOML" >&2
296+
exit 1
297+
fi
298+
IMPORT_STMTS+="; import torch; assert torch.__version__.split('+')[0] == '$EXPECTED_TORCH'"
299+
fi
300+
288301
if "$VENV/bin/python" -c "$IMPORT_STMTS" 2>/dev/null; then
289302
exit 0
290303
fi
@@ -324,9 +337,13 @@ echo "[2/3] Installing dependencies..."
324337
SYNC_CMD=(uv sync --active)
325338
if [[ "$ALL_GROUPS" == "true" ]]; then
326339
SYNC_CMD+=(--all-groups)
327-
# Exclude conflicting groups unless explicitly requested via --with-*
340+
# Exclude conflicting torch groups other than the one TORCH_GROUP or
341+
# --with-<group> selects, so --all-groups doesn't try to sync every
342+
# torch_2_* group at once. If both TORCH_GROUP and --with-<other-group>
343+
# name different groups, neither gets excluded here and `uv sync` itself
344+
# rejects the combination via its own conflicting-groups resolution.
328345
for GROUP in "${CONFLICTING_GROUPS[@]}"; do
329-
if [[ ! " ${EXTRA_GROUPS[*]:-} " == *" ${GROUP} "* ]]; then
346+
if [[ "$GROUP" != "${TORCH_GROUP:-}" ]] && [[ ! " ${EXTRA_GROUPS[*]:-} " == *" ${GROUP} "* ]]; then
330347
SYNC_CMD+=(--no-group "$GROUP")
331348
fi
332349
done
@@ -335,6 +352,14 @@ elif [[ ${#EXTRA_GROUPS[@]} -gt 0 ]]; then
335352
SYNC_CMD+=(--group "$GROUP")
336353
done
337354
fi
355+
# TORCH_GROUP is the single source of truth for torch pinning: append it
356+
# unconditionally (already covered under --all-groups by not being
357+
# excluded above). A disagreeing --with-<other-torch-group> ends up as a
358+
# second, different --group flag here, which `uv sync` itself rejects via
359+
# its conflicting-groups resolution rather than a bespoke check.
360+
if [[ -n "${TORCH_GROUP:-}" && "$ALL_GROUPS" != "true" ]]; then
361+
SYNC_CMD+=(--group "$TORCH_GROUP")
362+
fi
338363
# Apply explicit group exclusions (e.g., --without-coreai)
339364
if [[ ${#EXCLUDE_GROUPS[@]} -gt 0 ]]; then
340365
for GROUP in "${EXCLUDE_GROUPS[@]}"; do

src/coreai_opt/_utils/export_utils.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import torch
1010

1111
from coreai_opt._utils.torch_utils import is_tensor_on_cpu
12-
from coreai_opt.common import ExportBackend
12+
from coreai_opt.common import CoreMLExportError, ExportBackend
13+
from coreai_opt.config.spec import CompressionTargetTensor
14+
from coreai_opt.quantization.spec.granularity import PerTensorGranularity, QuantizationGranularity
1315

1416
COREML_SUPPORTED_WEIGHT_DTYPES: frozenset[torch.dtype] = frozenset(
1517
{
@@ -34,6 +36,47 @@
3436
}
3537
)
3638

39+
COREML_SUPPORTED_ACTIVATION_GRANULARITIES: frozenset[type[QuantizationGranularity]] = frozenset(
40+
{PerTensorGranularity}
41+
)
42+
43+
_COREML_SUPPORTED_DTYPES_BY_TARGET: dict[CompressionTargetTensor, frozenset[torch.dtype]] = {
44+
CompressionTargetTensor.WEIGHT: COREML_SUPPORTED_WEIGHT_DTYPES,
45+
CompressionTargetTensor.ACTIVATION: COREML_SUPPORTED_ACTIVATION_DTYPES,
46+
CompressionTargetTensor.LUT: COREML_SUPPORTED_LUT_DTYPES,
47+
}
48+
49+
50+
def validate_coreml_compatibility(
51+
target: CompressionTargetTensor,
52+
dtype: torch.dtype,
53+
context: str,
54+
granularity: QuantizationGranularity | None = None,
55+
) -> None:
56+
"""Raise CoreMLExportError if this weight/activation/LUT config isn't CoreML-exportable.
57+
58+
Centralizes every reason CoreML export can reject a quantization config, so
59+
new restrictions are added here once rather than at each call site.
60+
61+
Args:
62+
target (CompressionTargetTensor): Which tensor category is being checked.
63+
dtype (torch.dtype): The quantization dtype to validate.
64+
context (str): Human-readable description of what's being checked, used
65+
in the error message (e.g. "weight 'conv.weight' of module 'conv'").
66+
granularity (QuantizationGranularity | None): The quantization
67+
granularity, if applicable. Only checked for ACTIVATION — CoreML
68+
only supports per-tensor activation quantization.
69+
70+
Raises:
71+
CoreMLExportError: If the dtype or granularity isn't supported.
72+
"""
73+
if dtype not in _COREML_SUPPORTED_DTYPES_BY_TARGET[target]:
74+
raise CoreMLExportError.from_dtype(dtype, context)
75+
if target == CompressionTargetTensor.ACTIVATION and not isinstance(
76+
granularity, tuple(COREML_SUPPORTED_ACTIVATION_GRANULARITIES)
77+
):
78+
raise CoreMLExportError.from_config(granularity, context)
79+
3780

3881
def validate_mmap_backend_and_device(
3982
model: torch.nn.Module,

0 commit comments

Comments
 (0)