modelport-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,265 @@
1
+ """Hugging Face transformers image classifiers, from the Hub or a local folder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from types import SimpleNamespace
7
+ from typing import Any, Literal
8
+
9
+ from ..errors import MissingDependencyError
10
+ from ..manifest import DetectionPostprocess, DType, ImagePreprocess, InputSpec, ResizeSpec, Task
11
+ from . import SourceError, SourceModel, model_id
12
+
13
+ # PIL resampling codes used by transformers image processors.
14
+ _RESAMPLE: dict[int, Literal["nearest", "bilinear", "bicubic"]] = {
15
+ 0: "nearest",
16
+ 2: "bilinear",
17
+ 3: "bicubic",
18
+ }
19
+
20
+
21
+ def load_hf(repo: str, *, license: str | None = None, image_size: int | None = None) -> SourceModel:
22
+ """Load an image classifier or object detector with its image processor's preprocessing."""
23
+ try:
24
+ import torch
25
+ from transformers import AutoConfig, AutoImageProcessor, AutoModelForImageClassification
26
+ except ImportError as error:
27
+ raise MissingDependencyError("hf: sources", "hf") from error
28
+
29
+ try:
30
+ architectures = AutoConfig.from_pretrained(repo).architectures or []
31
+ except (OSError, ValueError) as error:
32
+ raise SourceError(f"could not load '{repo}': {error}") from error
33
+ if any(name.endswith("ForObjectDetection") for name in architectures):
34
+ return _load_detector(repo, license=license, image_size=image_size)
35
+
36
+ try:
37
+ processor = AutoImageProcessor.from_pretrained(repo)
38
+ model = AutoModelForImageClassification.from_pretrained(repo).eval()
39
+ except (OSError, ValueError) as error:
40
+ raise SourceError(f"could not load '{repo}' as an image classifier: {error}") from error
41
+
42
+ preprocess, (height, width) = preprocess_from_processor(processor)
43
+ config = model.config
44
+ labels = [str(config.id2label[i]) for i in range(config.num_labels)]
45
+
46
+ if license is None and not Path(repo).is_dir():
47
+ license = _hub_license(repo)
48
+ if license is None:
49
+ raise SourceError(f"no license found for '{repo}'. Pass it with --license, e.g. MIT")
50
+
51
+ return SourceModel(
52
+ module=_logits_only(model),
53
+ example_inputs=(torch.zeros(1, 3, height, width),),
54
+ id=model_id(repo),
55
+ task=Task.IMAGE_CLASSIFICATION,
56
+ license=license,
57
+ inputs=[
58
+ InputSpec(
59
+ name="pixel_values",
60
+ dtype=DType.FLOAT32,
61
+ shape=[1, 3, height, width],
62
+ layout="NCHW",
63
+ preprocess=preprocess,
64
+ )
65
+ ],
66
+ output_names=["logits"],
67
+ labels=labels,
68
+ name=repo.rstrip("/").split("/")[-1],
69
+ description=f"Hugging Face {type(model).__name__} from {repo}.",
70
+ source=f"hf:{repo}",
71
+ )
72
+
73
+
74
+ def _size_value(size: Any, key: str) -> int | None:
75
+ value = size.get(key) if isinstance(size, dict) else getattr(size, key, None)
76
+ return int(value) if value else None
77
+
78
+
79
+ def preprocess_from_processor(processor: Any) -> tuple[ImagePreprocess, tuple[int, int]]:
80
+ """Translate a transformers image processor into a manifest preprocessing rule.
81
+
82
+ Returns the rule and the final (height, width) of the input tensor.
83
+ """
84
+ resample = getattr(processor, "resample", 2)
85
+ code = int(getattr(resample, "value", resample))
86
+ method = _RESAMPLE.get(code)
87
+ if method is None:
88
+ raise SourceError(f"unsupported resample mode {resample!r}")
89
+ antialias = method != "nearest" # transformers resizes with PIL or antialiased torchvision
90
+
91
+ size = processor.size
92
+ height, width = _size_value(size, "height"), _size_value(size, "width")
93
+ shortest = _size_value(size, "shortest_edge")
94
+ crop_pct = getattr(processor, "crop_pct", None)
95
+ crop: tuple[int, int] | None = None
96
+
97
+ if height and width:
98
+ resize = ResizeSpec(size=(height, width), method=method, antialias=antialias)
99
+ final = (height, width)
100
+ elif shortest and crop_pct:
101
+ # ConvNext style: below 384, resize to shortest / crop_pct and crop back.
102
+ if shortest < 384:
103
+ resize = ResizeSpec(
104
+ shorter_side=int(shortest / crop_pct), method=method, antialias=antialias
105
+ )
106
+ crop = (shortest, shortest)
107
+ else:
108
+ resize = ResizeSpec(size=(shortest, shortest), method=method, antialias=antialias)
109
+ final = (shortest, shortest)
110
+ elif shortest:
111
+ resize = ResizeSpec(shorter_side=shortest, method=method, antialias=antialias)
112
+ crop_size = getattr(processor, "crop_size", None)
113
+ if not (getattr(processor, "do_center_crop", False) and crop_size):
114
+ raise SourceError("this processor gives a variable input size; a center crop is needed")
115
+ crop = (_size_value(crop_size, "height") or 0, _size_value(crop_size, "width") or 0)
116
+ final = crop
117
+ else:
118
+ raise SourceError(f"unsupported image processor size {size!r}")
119
+
120
+ if crop is None and getattr(processor, "do_center_crop", False):
121
+ crop_size = getattr(processor, "crop_size", None)
122
+ if crop_size:
123
+ crop = (_size_value(crop_size, "height") or 0, _size_value(crop_size, "width") or 0)
124
+ final = crop
125
+
126
+ do_rescale = getattr(processor, "do_rescale", True)
127
+ do_normalize = getattr(processor, "do_normalize", True)
128
+ preprocess = ImagePreprocess(
129
+ resize=resize,
130
+ center_crop=crop,
131
+ scale=float(processor.rescale_factor) if do_rescale else 1.0,
132
+ mean=_triple(processor.image_mean, "image_mean") if do_normalize else (0.0, 0.0, 0.0),
133
+ std=_triple(processor.image_std, "image_std") if do_normalize else (1.0, 1.0, 1.0),
134
+ )
135
+ return preprocess, final
136
+
137
+
138
+ def _triple(values: Any, name: str) -> tuple[float, float, float]:
139
+ numbers = [float(v) for v in values]
140
+ if len(numbers) != 3:
141
+ raise SourceError(f"{name} must have 3 values, got {len(numbers)}")
142
+ return numbers[0], numbers[1], numbers[2]
143
+
144
+
145
+ def _hub_license(repo: str) -> str | None:
146
+ try:
147
+ from huggingface_hub import model_info
148
+
149
+ card = model_info(repo).card_data
150
+ except Exception:
151
+ return None
152
+ license = getattr(card, "license", None) if card is not None else None
153
+ if isinstance(license, list):
154
+ license = license[0] if license else None
155
+ return str(license) if license else None
156
+
157
+
158
+ def _logits_only(model: Any) -> Any:
159
+ """Wrap a transformers model so forward(pixel_values) returns only the logits tensor."""
160
+ import torch
161
+
162
+ class LogitsOnly(torch.nn.Module):
163
+ def __init__(self, inner: Any) -> None:
164
+ super().__init__()
165
+ self.inner = inner
166
+
167
+ def forward(self, pixel_values: torch.Tensor) -> torch.Tensor:
168
+ return self.inner(pixel_values=pixel_values).logits
169
+
170
+ return LogitsOnly(model).eval()
171
+
172
+
173
+ def _load_detector(repo: str, *, license: str | None, image_size: int | None) -> SourceModel:
174
+ """DETR-family detectors (DETR, YOLOS, RT-DETR, D-FINE) exported at a fixed square size."""
175
+ import torch
176
+ from transformers import AutoImageProcessor, AutoModelForObjectDetection
177
+
178
+ try:
179
+ processor = AutoImageProcessor.from_pretrained(repo)
180
+ model = AutoModelForObjectDetection.from_pretrained(repo).eval()
181
+ except (OSError, ValueError) as error:
182
+ raise SourceError(f"could not load '{repo}' as an object detector: {error}") from error
183
+
184
+ try:
185
+ preprocess, (height, width) = preprocess_from_processor(processor)
186
+ except SourceError:
187
+ # Detectors usually accept any size; export at a fixed square instead.
188
+ size = image_size or _size_value(processor.size, "shortest_edge") or 512
189
+ fixed = SimpleNamespace(
190
+ resample=getattr(processor, "resample", 2),
191
+ do_rescale=getattr(processor, "do_rescale", True) is not False,
192
+ rescale_factor=getattr(processor, "rescale_factor", 1 / 255),
193
+ do_normalize=getattr(processor, "do_normalize", True) is not False,
194
+ image_mean=processor.image_mean,
195
+ image_std=processor.image_std,
196
+ size={"height": size, "width": size},
197
+ do_center_crop=False,
198
+ )
199
+ preprocess, (height, width) = preprocess_from_processor(fixed)
200
+ if image_size and (height, width) != (image_size, image_size):
201
+ raise SourceError(f"{repo} has a fixed input size of {height}x{width}")
202
+
203
+ wrapped = _detector_outputs(model)
204
+ example = torch.zeros(1, 3, height, width)
205
+ with torch.no_grad():
206
+ logits, _ = wrapped(example)
207
+ config = model.config
208
+ classes = int(logits.shape[-1])
209
+ background = classes == config.num_labels + 1
210
+ labels = [str(config.id2label[i]) for i in range(config.num_labels)]
211
+
212
+ if license is None and not Path(repo).is_dir():
213
+ license = _hub_license(repo)
214
+ if license is None:
215
+ raise SourceError(f"no license found for '{repo}'. Pass it with --license, e.g. MIT")
216
+
217
+ return SourceModel(
218
+ module=wrapped,
219
+ example_inputs=(example,),
220
+ id=model_id(repo),
221
+ task=Task.OBJECT_DETECTION,
222
+ license=license,
223
+ inputs=[
224
+ InputSpec(
225
+ name="pixel_values",
226
+ dtype=DType.FLOAT32,
227
+ shape=[1, 3, height, width],
228
+ layout="NCHW",
229
+ preprocess=preprocess,
230
+ )
231
+ ],
232
+ output_names=["logits", "pred_boxes"],
233
+ labels=labels,
234
+ name=repo.rstrip("/").split("/")[-1],
235
+ description=f"Hugging Face {type(model).__name__} from {repo}, at {width}x{height}.",
236
+ source=f"hf:{repo}",
237
+ detection=DetectionPostprocess(
238
+ format="detr",
239
+ boxes_output="pred_boxes",
240
+ activation="softmax" if background else "sigmoid",
241
+ background_class=background,
242
+ box_format="cxcywh",
243
+ normalized=True,
244
+ score_threshold=0.5,
245
+ # DETR-style models predict each object once, so no suppression is needed.
246
+ iou_threshold=1.0,
247
+ max_detections=int(logits.shape[1]),
248
+ ),
249
+ )
250
+
251
+
252
+ def _detector_outputs(model: Any) -> Any:
253
+ """Wrap a detector so forward(pixel_values) returns (logits, pred_boxes)."""
254
+ import torch
255
+
256
+ class DetectorOutputs(torch.nn.Module):
257
+ def __init__(self, inner: Any) -> None:
258
+ super().__init__()
259
+ self.inner = inner
260
+
261
+ def forward(self, pixel_values: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
262
+ out = self.inner(pixel_values=pixel_values)
263
+ return out.logits, out.pred_boxes
264
+
265
+ return DetectorOutputs(model).eval()
@@ -0,0 +1,78 @@
1
+ """torchvision classification models with their ImageNet preprocessing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from ..errors import MissingDependencyError
8
+ from ..manifest import DType, ImagePreprocess, InputSpec, ResizeSpec, Task
9
+ from . import SourceError, SourceModel
10
+
11
+ _METHODS: dict[str, Literal["bilinear", "bicubic", "nearest"]] = {
12
+ "bilinear": "bilinear",
13
+ "bicubic": "bicubic",
14
+ "nearest": "nearest",
15
+ }
16
+
17
+
18
+ def load_torchvision(name: str, *, pretrained: bool = True) -> SourceModel:
19
+ """Load a torchvision classifier.
20
+
21
+ `pretrained=False` keeps the weights' metadata but skips the download. Tests use it.
22
+ """
23
+ try:
24
+ import torch
25
+ import torchvision.models as models
26
+ except ImportError as error:
27
+ raise MissingDependencyError("torchvision: sources", "torchvision") from error
28
+
29
+ try:
30
+ weights = models.get_model_weights(name)["DEFAULT"]
31
+ except ValueError as error:
32
+ raise SourceError(f"torchvision has no model named '{name}'") from error
33
+
34
+ transform = weights.transforms()
35
+ if type(transform).__name__ != "ImageClassification":
36
+ raise SourceError(
37
+ f"torchvision:{name} is a {type(transform).__name__} model. "
38
+ "Only image classification is supported for now."
39
+ )
40
+
41
+ method = _METHODS.get(transform.interpolation.value)
42
+ if method is None:
43
+ raise SourceError(f"unsupported interpolation {transform.interpolation}")
44
+ crop = int(transform.crop_size[0])
45
+ preprocess = ImagePreprocess(
46
+ # torchvision resizes PIL images with PIL, which always antialiases.
47
+ resize=ResizeSpec(
48
+ shorter_side=int(transform.resize_size[0]),
49
+ method=method,
50
+ antialias=method != "nearest",
51
+ ),
52
+ center_crop=(crop, crop),
53
+ mean=tuple(transform.mean),
54
+ std=tuple(transform.std),
55
+ )
56
+ module = models.get_model(name, weights=weights if pretrained else None).eval()
57
+ return SourceModel(
58
+ module=module,
59
+ example_inputs=(torch.zeros(1, 3, crop, crop),),
60
+ id=name.lower(),
61
+ task=Task.IMAGE_CLASSIFICATION,
62
+ # torchvision code and its pretrained weights are released under BSD-3-Clause.
63
+ license="BSD-3-Clause",
64
+ inputs=[
65
+ InputSpec(
66
+ name="pixel_values",
67
+ dtype=DType.FLOAT32,
68
+ shape=[1, 3, crop, crop],
69
+ layout="NCHW",
70
+ preprocess=preprocess,
71
+ )
72
+ ],
73
+ output_names=["logits"],
74
+ labels=list(weights.meta["categories"]),
75
+ name=name,
76
+ description=f"torchvision {name} with {weights.name} weights.",
77
+ source=f"torchvision:{name}",
78
+ )
modelport/verify.py ADDED
@@ -0,0 +1,92 @@
1
+ """Check every variant of a bundle against its golden data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+
10
+ from .bundle import Bundle
11
+ from .errors import MissingDependencyError, ModelPortError
12
+ from .golden import golden_arrays
13
+ from .manifest import ClassificationPostprocess, Tolerance
14
+ from .runtimes import RuntimeNotSupported, run_variant
15
+
16
+
17
+ class VerifyError(ModelPortError):
18
+ """The bundle cannot be verified at all."""
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class OutputCheck:
23
+ name: str
24
+ max_abs_diff: float
25
+ cosine: float
26
+ within_tolerance: bool
27
+ top1_match: bool | None = None
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class VariantResult:
32
+ variant_id: str
33
+ runtime: str
34
+ tolerance: Tolerance
35
+ outputs: list[OutputCheck] = field(default_factory=list)
36
+ skipped: str | None = None
37
+
38
+ @property
39
+ def passed(self) -> bool:
40
+ return self.skipped is None and all(
41
+ o.within_tolerance and o.top1_match is not False for o in self.outputs
42
+ )
43
+
44
+
45
+ def compare(
46
+ name: str, got: np.ndarray, expected: np.ndarray, tolerance: Tolerance, classify: bool
47
+ ) -> OutputCheck:
48
+ if got.shape != expected.shape:
49
+ raise VerifyError(f"output '{name}' has shape {got.shape}, golden has {expected.shape}")
50
+ a = got.astype(np.float64).ravel()
51
+ b = expected.astype(np.float64).ravel()
52
+ diff = np.abs(a - b)
53
+ within = bool(np.all(diff <= tolerance.atol + tolerance.rtol * np.abs(b)))
54
+ norms = np.linalg.norm(a) * np.linalg.norm(b)
55
+ cosine = float(np.dot(a, b) / norms) if norms > 0 else float(np.array_equal(a, b))
56
+ top1 = None
57
+ if classify:
58
+ top1 = bool(np.array_equal(got.argmax(axis=-1), expected.argmax(axis=-1)))
59
+ return OutputCheck(name, float(diff.max(initial=0.0)), cosine, within, top1)
60
+
61
+
62
+ def verify_bundle(path: str | Path) -> list[VariantResult]:
63
+ bundle = Bundle(path)
64
+ if not bundle.manifest_path.is_file():
65
+ raise VerifyError(f"{bundle.manifest_path} not found")
66
+ manifest = bundle.read_manifest()
67
+ problems = bundle.problems(manifest)
68
+ if problems:
69
+ raise VerifyError("bundle files do not match the manifest:\n " + "\n ".join(problems))
70
+ if manifest.golden is None:
71
+ raise VerifyError(f"{manifest.id} has no golden data to verify against")
72
+
73
+ inputs, expected = golden_arrays(bundle, manifest)
74
+ classify = {
75
+ o.name for o in manifest.outputs if isinstance(o.postprocess, ClassificationPostprocess)
76
+ }
77
+ results = []
78
+ for variant in manifest.variants:
79
+ tolerance = variant.tolerance or manifest.golden.tolerance
80
+ try:
81
+ got = run_variant(bundle, manifest, variant, inputs)
82
+ except (MissingDependencyError, RuntimeNotSupported) as error:
83
+ results.append(
84
+ VariantResult(variant.id, variant.runtime, tolerance, skipped=str(error))
85
+ )
86
+ continue
87
+ checks = [
88
+ compare(name, got[name], want, tolerance, name in classify)
89
+ for name, want in expected.items()
90
+ ]
91
+ results.append(VariantResult(variant.id, variant.runtime, tolerance, checks))
92
+ return results
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.5
2
+ Name: modelport-cli
3
+ Version: 0.1.0
4
+ Summary: Prepare PyTorch, Hugging Face, and GGUF models for Flutter apps: export, verify, pack, publish.
5
+ Project-URL: Homepage, https://github.com/ayanparvaiz/modelport
6
+ Project-URL: Documentation, https://ayanparvaiz.github.io/modelport/
7
+ Project-URL: Issues, https://github.com/ayanparvaiz/modelport/issues
8
+ Project-URL: Changelog, https://github.com/ayanparvaiz/modelport/blob/main/python/CHANGELOG.md
9
+ Author: Ayan Parvaiz
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: ai,dart,executorch,flutter,gguf,mobile,on-device,onnx
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development :: Code Generators
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: numpy>=1.26
24
+ Requires-Dist: pillow>=10
25
+ Requires-Dist: pydantic>=2.7
26
+ Requires-Dist: rich>=13
27
+ Requires-Dist: typer>=0.12
28
+ Provides-Extra: executorch
29
+ Requires-Dist: executorch>=1.0; extra == 'executorch'
30
+ Requires-Dist: torch>=2.9; extra == 'executorch'
31
+ Provides-Extra: gguf
32
+ Requires-Dist: gguf>=0.17; extra == 'gguf'
33
+ Provides-Extra: hf
34
+ Requires-Dist: huggingface-hub>=0.30; extra == 'hf'
35
+ Requires-Dist: torch>=2.9; extra == 'hf'
36
+ Requires-Dist: transformers>=4.50; extra == 'hf'
37
+ Provides-Extra: onnx
38
+ Requires-Dist: onnx>=1.17; extra == 'onnx'
39
+ Requires-Dist: onnxruntime>=1.20; extra == 'onnx'
40
+ Requires-Dist: onnxscript>=0.5; extra == 'onnx'
41
+ Requires-Dist: torch>=2.9; extra == 'onnx'
42
+ Provides-Extra: torchvision
43
+ Requires-Dist: torch>=2.9; extra == 'torchvision'
44
+ Requires-Dist: torchvision>=0.24; extra == 'torchvision'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # modelport-cli
48
+
49
+ Prepare models for Flutter apps. Part of [ModelPort](https://github.com/ayanparvaiz/modelport).
50
+
51
+ Install `modelport-cli`; the command and the Python package are both called `modelport`.
52
+
53
+ > **Status: 0.1.0, alpha.** Works end to end and is tested on a real Android phone. The spec may still change before 1.0.
54
+
55
+ The CLI converts PyTorch, Hugging Face, and torchvision models into mobile formats, checks that the converted model gives the same output as the original, and writes a `modelport.json` manifest that the ModelPort Dart packages read.
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install "modelport-cli[onnx,torchvision]"
61
+ ```
62
+
63
+ | Extra | Adds | Needed for |
64
+ |---|---|---|
65
+ | `onnx` | onnx, onnxruntime, onnxscript, torch | ONNX export, quantize, verify |
66
+ | `executorch` | executorch, torch | ExecuTorch export and verify |
67
+ | `torchvision` | torchvision, torch | `torchvision:` sources |
68
+ | `hf` | transformers, huggingface_hub, torch | `hf:` sources and `publish` |
69
+ | `gguf` | gguf | Inspecting GGUF files |
70
+
71
+ Run `modelport doctor` to see what is installed.
72
+
73
+ ## Quick start
74
+
75
+ ```bash
76
+ modelport export torchvision:mobilenet_v3_small --target onnx,executorch
77
+ modelport quantize dist/mobilenet_v3_small --fp16 --int8
78
+ modelport verify dist/mobilenet_v3_small
79
+ modelport publish dist/mobilenet_v3_small --hf your-name/mobilenet_v3_small
80
+ ```
81
+
82
+ `verify` runs every variant on the saved golden input and compares it with PyTorch's output:
83
+
84
+ ```
85
+ variant ┃ output ┃ max diff ┃ cosine ┃ top-1 ┃ result
86
+ ━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━━
87
+ onnx-fp32 │ logits │ 3.34e-05 │ 1.000000 │ same │ pass
88
+ executorch-xnnpack-fp32 │ logits │ 3.34e-05 │ 1.000000 │ same │ pass
89
+ onnx-fp16 │ logits │ 1.05e-01 │ 0.999952 │ same │ pass
90
+ onnx-int8 │ logits │ 1.27e-01 │ 0.999933 │ same │ pass
91
+ ```
92
+
93
+ ## Commands
94
+
95
+ | Command | What it does |
96
+ |---|---|
97
+ | `export SOURCE` | Convert a model and write a bundle with `modelport.json`, labels, and golden data. |
98
+ | `quantize BUNDLE` | Add `--fp16` and `--int8` ONNX variants and record how far each drifts. |
99
+ | `verify BUNDLE` | Run every variant on the golden input and compare with the expected output. |
100
+ | `pack BUNDLE` | Refresh sizes and hashes after manual edits and list unlisted files. |
101
+ | `publish BUNDLE --hf org/name` | Upload to the Hugging Face Hub. Log in first with `hf auth login`. |
102
+ | `inspect FILE` | Show inputs, outputs, and metadata of an `.onnx`, `.pte`, or `.gguf` file. |
103
+ | `validate MANIFEST...` | Check manifests against the spec. |
104
+ | `schema` | Print the manifest JSON Schema. |
105
+ | `doctor` | Check Python, optional packages, and disk space. |
106
+
107
+ ## Sources
108
+
109
+ | Source | Example |
110
+ |---|---|
111
+ | torchvision classifier | `torchvision:efficientnet_b0` |
112
+ | Hugging Face image classifier | `hf:facebook/deit-tiny-patch16-224` |
113
+ | Your own model | `file:my_model.py:build` |
114
+
115
+ For `file:`, write a function that returns a `SourceModel`:
116
+
117
+ ```python
118
+ import torch
119
+ from modelport.manifest import DType, ImagePreprocess, InputSpec, ResizeSpec, Task
120
+ from modelport.sources import SourceModel
121
+
122
+
123
+ def build() -> SourceModel:
124
+ net = MyNet()
125
+ net.load_state_dict(torch.load("weights.pt", weights_only=True))
126
+ return SourceModel(
127
+ module=net.eval(),
128
+ example_inputs=(torch.zeros(1, 3, 224, 224),),
129
+ id="my_net",
130
+ task=Task.IMAGE_CLASSIFICATION,
131
+ license="MIT",
132
+ inputs=[
133
+ InputSpec(
134
+ name="pixel_values",
135
+ dtype=DType.FLOAT32,
136
+ shape=[1, 3, 224, 224],
137
+ layout="NCHW",
138
+ preprocess=ImagePreprocess(resize=ResizeSpec(size=(224, 224))),
139
+ )
140
+ ],
141
+ output_names=["logits"],
142
+ labels=["cat", "dog"],
143
+ )
144
+ ```
145
+
146
+ `file:` runs the code in that file, so only use files you trust.
147
+
148
+ ## Notes on quantization
149
+
150
+ - `--fp16` halves the size and keeps inputs and outputs in float32.
151
+ - `--int8` quantizes only MatMul and Gemm weights. Dynamic int8 on convolutions changed MobileNetV3's top-1 class in testing, so convolution-heavy models shrink less. Transformers shrink to about a third.
152
+ - Each new variant gets a tolerance of twice its measured error, so devices are checked against a realistic bar.
@@ -0,0 +1,41 @@
1
+ modelport/__init__.py,sha256=6pDHHCifeMBbty1n-8CVWnv7hpqG75oK_H4gsqGMsmA,73
2
+ modelport/bundle.py,sha256=VjkKeBwDQfyNGLzsUk-NlkoN3iXlhlXBAG5AG9aCaN4,3278
3
+ modelport/cli.py,sha256=jfhfcqzl3RdVkimipNKnQy6mdeKrdiWmKW4ZEJrq_do,19994
4
+ modelport/doctor.py,sha256=Xqt6tz9QdxIvMmrtntBTMmK2o0iKil1PmvzuZ9h7QCk,2633
5
+ modelport/errors.py,sha256=8M2nQDETQT2P_zghN-3zeLCBpSA60asB0xJIrHzD8pY,562
6
+ modelport/gguf_import.py,sha256=bF3R8zvzBZoFqyBrXEpjEMOzCVwXPtREcVxFOk31KZM,6807
7
+ modelport/golden.py,sha256=zFPcD-MYSMmJcJAa9Sh6lwN8P1D8oXrerY4D5cCRXNo,4161
8
+ modelport/hashing.py,sha256=fIIZ_XNK1neFFjbxRKheZ6hwTY-pFuYpC0FG_1t3-Go,411
9
+ modelport/pack.py,sha256=yRYfslaBLEBoBynzvFeFVUUShblx5EGj_LqIiA0vdiA,1682
10
+ modelport/pipeline.py,sha256=HdzUS3xdleY-Zxe3mkNPTaib5E_Oo72RbVoFVKW6cqs,3787
11
+ modelport/preprocess.py,sha256=NzZOD-4cgdx8uBFEBnMPNCTz4zS5BzO-Li3vlwxszHY,5114
12
+ modelport/publish.py,sha256=7YzEbt3uZHU119-5M4IMY-99pMPSTi2JffDwCJ0aaCE,6853
13
+ modelport/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ modelport/quantize.py,sha256=E1mBOlIBaE64uNF-kcCWzN_lJbC-5LGreT6kBZ-cZkE,4779
15
+ modelport/runtimes.py,sha256=wAd_-I1e_8XyyWow4KPJi1I2wWUcnMpSdjGAEFrbEJA,2211
16
+ modelport/verify.py,sha256=GMvXQr0RwSSEgjcZOOOX7cjrb_gkqXJk-I401OJGEZ8,3184
17
+ modelport/codegen/__init__.py,sha256=DjDmljKF8DEeHiIJyuW72xARY2ipZ57u_JpHZ1SvTpc,38
18
+ modelport/codegen/dart.py,sha256=DsGA4jXZxkxBKnBnOt1rk3gCdZnNfsgCY_BnbRMBe00,7036
19
+ modelport/exporters/__init__.py,sha256=l7eLwe-YGqYbkQGFusVGWAdEXFkz1CLQNJNalM-ipMU,1488
20
+ modelport/exporters/executorch.py,sha256=Oeyw6zE4Me9X5jYEquhptoeZBNpgZ8OesS130-ZWeyQ,1744
21
+ modelport/exporters/onnx.py,sha256=c9QeMZhloWVIJITkX5G3lG5pHJREheXYgpfDal9TLf8,2429
22
+ modelport/inspection/__init__.py,sha256=AgOK7o-dub3uX18fmCbNtL6B4eLTUhwNCh048erxhAU,2263
23
+ modelport/inspection/executorch.py,sha256=uGytgcUAq2hmV-MHXWPkgrfIoTynoYgnoPg31sW96Ho,1385
24
+ modelport/inspection/gguf.py,sha256=wWHj-Wcf3_ty_xzcNYXMMDYJMBdYcE6_0y9L5dgz1Ww,1362
25
+ modelport/inspection/onnx.py,sha256=sM9KDxNOLtoW6v_YfiUUydjWqGY2vAVB0uzAm0VAXRs,1791
26
+ modelport/manifest/__init__.py,sha256=9AHphUN9Xn_VOZivTYqQs1N7ZF7op6i2sHdr6R_EXOQ,841
27
+ modelport/manifest/base.py,sha256=f7iLoqm-ZRN1Y9HciAnAXREHaaiC3QmD_wzg-uAWpUA,586
28
+ modelport/manifest/files.py,sha256=RQ7cD3ox1s2loYqrFZlalCjy-lwrfJMYQZDAtmK8tII,2150
29
+ modelport/manifest/models.py,sha256=-ibROqatIlHeGusZ7cNSsuQAeiMY9YXq4qd4UxN-JDs,9394
30
+ modelport/manifest/postprocess.py,sha256=9Pm0QtycS3XP-fFqLl-m4wZ0DAfeq3lm4HXRQYJJzNs,3046
31
+ modelport/manifest/schema.py,sha256=Tw_Few0kpkWCdnNJwafPSHp5B-xvGWXEten6D-WhxZg,1586
32
+ modelport/manifest/tensors.py,sha256=aT6Rq80Q9h7zT1oKSWSFMVo-tjg5XTLle2ysh8XT5zc,5776
33
+ modelport/sources/__init__.py,sha256=rPRFaB1XhxCG79Nc11_XW_D86sfmH-zFEidbqCPkPc0,2700
34
+ modelport/sources/file.py,sha256=kxaYd5aC2QzskrOLX6fsffOT-4_oX_5qs8xJrcUD7wU,1672
35
+ modelport/sources/hf.py,sha256=kPhIY9KbAdgv_tNc2dDBfgJxQ2ieBKFf3z12osEzQiI,10536
36
+ modelport/sources/torchvision.py,sha256=wFxfoRY2POeyoY7YSy1TCEnQa1Q_T00Lo16IW_kfQ_Y,2735
37
+ modelport_cli-0.1.0.dist-info/METADATA,sha256=rDUkuPZxeU5yb0kKNtaQcs-TirKElIMe6Yte76EJVw0,6406
38
+ modelport_cli-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
39
+ modelport_cli-0.1.0.dist-info/entry_points.txt,sha256=63cpIl0-7KbhdI3Q24NDPfW9jW68A_wIhV1cLPy_p9E,48
40
+ modelport_cli-0.1.0.dist-info/licenses/LICENSE,sha256=yVuuHRzgI17MzTVgt3LsHvuX80innw--CmNPDCzO_iw,11358
41
+ modelport_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ modelport = modelport.cli:app