Merge nucleic/sleek-ember-seal-uady into dev

This commit is contained in:
2026-07-30 21:39:52 -07:00
parent 40176d0c76
commit a374fa40b6
3 changed files with 72 additions and 10 deletions
+4 -2
View File
@@ -234,8 +234,10 @@ ml/purpose-classifier/venv/bin/python ml/purpose-classifier/quantize_coreml.py \
--overwrite-output --overwrite-output
``` ```
Activation calibration is grouped to keep temporary Core ML packages bounded and prints Activation calibration writes its temporary packages under the candidate output directory
progress while it runs. The candidate uses per-tensor asymmetric uint8 activations, and removes each package immediately after prediction; this avoids Core ML Tools retaining
one full weight copy per calibration step until process exit. It prints progress while it
runs. The candidate uses per-tensor asymmetric uint8 activations,
per-channel symmetric int8 linear weights, and per-tensor asymmetric uint8 embedding per-channel symmetric int8 linear weights, and per-tensor asymmetric uint8 embedding
weights. It fails the command if the resulting package exceeds 25 MiB. weights. It fails the command if the resulting package exceeds 25 MiB.
+51 -8
View File
@@ -4,10 +4,13 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import gc
import hashlib import hashlib
import shutil import shutil
import sys import sys
import tempfile
from collections import Counter from collections import Counter
from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import Any, Sequence from typing import Any, Sequence
@@ -42,6 +45,38 @@ def _tree_sha256(package: Path) -> str:
return digest.hexdigest() return digest.hexdigest()
@contextmanager
def _bounded_calibration_packages(debugger_type: Any, temporary_root: Path):
"""Eagerly remove Core ML Tools' per-prediction temporary packages.
Core ML Tools registers these packages for process-exit cleanup. Activation
calibration creates one package per intermediate-output group per record, so retaining
all of them can consume tens of gigabytes before the process exits.
"""
original_predict = debugger_type.predict_intermediate_outputs
previous_tempdir = tempfile.tempdir
def predict_and_cleanup(*args: Any, **kwargs: Any) -> Any:
try:
return original_predict(*args, **kwargs)
finally:
gc.collect()
for package in temporary_root.glob("*.mlpackage"):
shutil.rmtree(package)
temporary_root.mkdir(parents=True, exist_ok=True)
tempfile.tempdir = str(temporary_root)
debugger_type.predict_intermediate_outputs = predict_and_cleanup
try:
yield
finally:
debugger_type.predict_intermediate_outputs = original_predict
tempfile.tempdir = previous_tempdir
for package in temporary_root.glob("*.mlpackage"):
shutil.rmtree(package)
def _optimization_configs(optimize: Any) -> tuple[Any, Any]: def _optimization_configs(optimize: Any) -> tuple[Any, Any]:
"""Return the Core ML analogue of the accepted ONNX QDQ policy.""" """Return the Core ML analogue of the accepted ONNX QDQ policy."""
@@ -101,6 +136,9 @@ def quantize(args: argparse.Namespace) -> dict[str, Any]:
import coremltools as ct import coremltools as ct
import coremltools.optimize as cto import coremltools.optimize as cto
import torch import torch
from coremltools.optimize.coreml.experimental._model_debugger import (
ModelDebugger,
)
from transformers import AutoTokenizer from transformers import AutoTokenizer
except ImportError as exc: except ImportError as exc:
raise DataError( raise DataError(
@@ -147,12 +185,18 @@ def quantize(args: argparse.Namespace) -> dict[str, Any]:
f"got {sorted(input_names)}" f"got {sorted(input_names)}"
) )
activation_config, weight_config = _optimization_configs(cto) activation_config, weight_config = _optimization_configs(cto)
activation_quantized = cto.coreml.linear_quantize_activations( args.output.parent.mkdir(parents=True, exist_ok=True)
source_model, with tempfile.TemporaryDirectory(
activation_config, prefix=".purpose-coreml-calibration-",
sample_data, dir=args.output.parent,
calibration_op_group_size=args.calibration_op_group_size, ) as temporary:
) with _bounded_calibration_packages(ModelDebugger, Path(temporary)):
activation_quantized = cto.coreml.linear_quantize_activations(
source_model,
activation_config,
sample_data,
calibration_op_group_size=args.calibration_op_group_size,
)
print("Core ML weight quantization: W8", flush=True) print("Core ML weight quantization: W8", flush=True)
quantized = cto.coreml.linear_quantize_weights( quantized = cto.coreml.linear_quantize_weights(
activation_quantized, activation_quantized,
@@ -168,7 +212,6 @@ def quantize(args: argparse.Namespace) -> dict[str, Any]:
shutil.rmtree(args.output) shutil.rmtree(args.output)
else: else:
args.output.unlink() args.output.unlink()
args.output.parent.mkdir(parents=True, exist_ok=True)
quantized.save(str(args.output)) quantized.save(str(args.output))
manifest = _package_manifest(args.output) manifest = _package_manifest(args.output)
@@ -226,7 +269,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument("--calibration-records", type=int, default=256) parser.add_argument("--calibration-records", type=int, default=256)
parser.add_argument("--calibration-seed", type=int, default=20260730) parser.add_argument("--calibration-seed", type=int, default=20260730)
parser.add_argument("--calibration-op-group-size", type=int, default=32) parser.add_argument("--calibration-op-group-size", type=int, default=-1)
parser.add_argument( parser.add_argument(
"--shipping-budget-bytes", "--shipping-budget-bytes",
type=int, type=int,
+17
View File
@@ -74,6 +74,23 @@ class CoreMLQuantizationConfigTests(unittest.TestCase):
with self.assertRaisesRegex(DataError, "must not overwrite"): with self.assertRaisesRegex(DataError, "must not overwrite"):
quantize_coreml._validate_args(args) quantize_coreml._validate_args(args)
def test_calibration_packages_are_removed_after_each_prediction(self):
class FakeDebugger:
def predict_intermediate_outputs(self):
package = Path(tempfile.mkdtemp(suffix=".mlpackage"))
(package / "weight.bin").write_bytes(b"weights")
return {"output": np.array([1.0])}
with tempfile.TemporaryDirectory() as temp:
root = Path(temp) / "calibration"
with quantize_coreml._bounded_calibration_packages(
FakeDebugger,
root,
):
output = FakeDebugger().predict_intermediate_outputs()
self.assertEqual([1.0], output["output"].tolist())
self.assertEqual([], list(root.glob("*.mlpackage")))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()