Edit on GitHub

model_signing.hashing

High level API for the hashing interface of model_signing library.

Hashing is used both for signing and verification and users should ensure that the same configuration is used in both cases.

The module could also be used to just hash a single model, without signing it:

This module allows setting up the hashing configuration to a single variable and then sharing it between signing and verification.

hashing_config = model_signing.hashing.Config().set_ignored_paths(
    paths=["README.md"], ignore_git_paths=True
)

signing_config = (
    model_signing.signing.Config()
    .use_elliptic_key_signer(private_key="key")
    .set_hashing_config(hashing_config)
)

verifying_config = (
    model_signing.verifying.Config()
    .use_elliptic_key_verifier(public_key="key.pub")
    .set_hashing_config(hashing_config)
)

The API defined here is stable and backwards compatible.

  1# Copyright 2024 The Sigstore Authors
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#      http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""High level API for the hashing interface of `model_signing` library.
 16
 17Hashing is used both for signing and verification and users should ensure that
 18the same configuration is used in both cases.
 19
 20The module could also be used to just hash a single model, without signing it:
 21
 22```python
 23model_signing.hashing.hash(model_path)
 24```
 25
 26This module allows setting up the hashing configuration to a single variable and
 27then sharing it between signing and verification.
 28
 29```python
 30hashing_config = model_signing.hashing.Config().set_ignored_paths(
 31    paths=["README.md"], ignore_git_paths=True
 32)
 33
 34signing_config = (
 35    model_signing.signing.Config()
 36    .use_elliptic_key_signer(private_key="key")
 37    .set_hashing_config(hashing_config)
 38)
 39
 40verifying_config = (
 41    model_signing.verifying.Config()
 42    .use_elliptic_key_verifier(public_key="key.pub")
 43    .set_hashing_config(hashing_config)
 44)
 45```
 46
 47The API defined here is stable and backwards compatible.
 48"""
 49
 50from collections.abc import Callable, Iterable
 51import os
 52import pathlib
 53from typing import Literal, Self, TypeAlias
 54
 55import blake3
 56
 57from model_signing import manifest
 58from model_signing._hashing import hashing
 59from model_signing._hashing import io
 60from model_signing._hashing import memory
 61from model_signing._serialization import file
 62from model_signing._serialization import file_shard
 63
 64
 65# Type alias to support `os.PathLike`, `str` and `bytes` objects in the API
 66# `TypeAlias` is deprecated in Python 3.12 in favor of `type`, which we can use
 67# once Python 3.12 is the minimum supported version
 68PathLike: TypeAlias = str | bytes | os.PathLike
 69
 70
 71def hash(model_path: PathLike) -> manifest.Manifest:
 72    """Hashes a model using the default configuration.
 73
 74    Hashing is the shared part between signing and verification and is also
 75    expected to be the slowest component. When serializing a model, we need to
 76    spend time proportional to the model size on disk.
 77
 78    This method returns a "manifest" of the model. A manifest is a collection of
 79    every object in the model, paired with the corresponding hash. Currently, we
 80    consider an object in the model to be either a file or a shard of the file.
 81    Large models with large files will be hashed much faster when every shard is
 82    hashed in parallel, at the cost of generating a larger payload for the
 83    signature. In future releases we could support hashing individual tensors or
 84    tensor slices for further speed optimizations for very large models.
 85
 86    Args:
 87        model_path: The path to the model to hash.
 88
 89    Returns:
 90        A manifest of the hashed model.
 91    """
 92    return Config().hash(model_path)
 93
 94
 95class Config:
 96    """Configuration to use when hashing models.
 97
 98    Hashing is the shared part between signing and verification and is also
 99    expected to be the slowest component. When serializing a model, we need to
100    spend time proportional to the model size on disk.
101
102    Hashing builds a "manifest" of the model. A manifest is a collection of
103    every object in the model, paired with the corresponding hash. Currently, we
104    consider an object in the model to be either a file or a shard of the file.
105    Large models with large files will be hashed much faster when every shard is
106    hashed in parallel, at the cost of generating a larger payload for the
107    signature. In future releases we could support hashing individual tensors or
108    tensor slices for further speed optimizations for very large models.
109
110    This configuration class supports configuring the hashing granularity. By
111    default, we hash at file level granularity.
112
113    This configuration class also supports configuring the hash method used to
114    generate the hash for every object in the model. We currently support
115    SHA256, BLAKE2, and BLAKE3, with SHA256 being the default.
116
117    This configuration class also supports configuring which paths from the
118    model directory should be ignored. These are files that doesn't impact the
119    behavior of the model, or files that won't be distributed with the model. By
120    default, only files that are associated with a git repository (`.git`,
121    `.gitattributes`, `.gitignore`, etc.) are ignored.
122    """
123
124    def __init__(self):
125        """Initializes the default configuration for hashing."""
126        self._ignored_paths = frozenset()
127        self._ignore_git_paths = True
128        self.use_file_serialization()
129        self._allow_symlinks = False
130
131    def hash(
132        self,
133        model_path: PathLike,
134        *,
135        files_to_hash: Iterable[PathLike] | None = None,
136    ) -> manifest.Manifest:
137        """Hashes a model using the current configuration."""
138        # All paths in ``_ignored_paths`` are expected to be relative to the
139        # model directory. Join them to ``model_path`` and ensure they do not
140        # escape it.
141        model_path = pathlib.Path(model_path)
142        ignored_paths = []
143        for p in self._ignored_paths:
144            full = model_path / p
145            try:
146                full.relative_to(model_path)
147            except ValueError:
148                continue
149            ignored_paths.append(full)
150
151        if self._ignore_git_paths:
152            ignored_paths.extend(
153                [
154                    model_path / p
155                    for p in [
156                        ".git/",
157                        ".gitattributes",
158                        ".github/",
159                        ".gitignore",
160                    ]
161                ]
162            )
163
164        self._serializer.set_allow_symlinks(self._allow_symlinks)
165
166        return self._serializer.serialize(
167            pathlib.Path(model_path),
168            ignore_paths=ignored_paths,
169            files_to_hash=files_to_hash,
170        )
171
172    def _build_stream_hasher(
173        self,
174        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
175    ) -> hashing.StreamingHashEngine:
176        """Builds a streaming hasher from a constant string.
177
178        Args:
179            hashing_algorithm: The hashing algorithm to use.
180
181        Returns:
182            An instance of the requested hasher.
183        """
184        match hashing_algorithm:
185            case "sha256":
186                return memory.SHA256()
187            case "blake2":
188                return memory.BLAKE2()
189            case "blake3":
190                return memory.BLAKE3()
191            case _:
192                raise ValueError(
193                    f"Unsupported hashing method {hashing_algorithm}"
194                )
195
196    def _build_file_hasher_factory(
197        self,
198        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
199        chunk_size: int = 1048576,
200        max_workers: int | None = None,
201    ) -> Callable[[pathlib.Path], io.FileHasher]:
202        """Builds the hasher factory for a serialization by file.
203
204        Args:
205            hashing_algorithm: The hashing algorithm to use to hash a file.
206            chunk_size: The amount of file to read at once. Default is 1MB. A
207              special value of 0 signals to attempt to read everything in a
208              single call. This is ignored for BLAKE3.
209            max_workers: Maximum number of workers to use in parallel. Defaults
210              to the number of logical cores. Only relevant for BLAKE3.
211
212        Returns:
213            The hasher factory that should be used by the active serialization
214            method.
215        """
216        if max_workers is None:
217            max_workers = blake3.blake3.AUTO
218
219        def _factory(path: pathlib.Path) -> io.FileHasher:
220            if hashing_algorithm == "blake3":
221                return io.Blake3FileHasher(path, max_threads=max_workers)
222            hasher = self._build_stream_hasher(hashing_algorithm)
223            return io.SimpleFileHasher(path, hasher, chunk_size=chunk_size)
224
225        return _factory
226
227    def _build_sharded_file_hasher_factory(
228        self,
229        hashing_algorithm: Literal["sha256", "blake2"] = "sha256",
230        chunk_size: int = 1048576,
231        shard_size: int = 1_000_000_000,
232    ) -> Callable[[pathlib.Path, int, int], io.ShardedFileHasher]:
233        """Builds the hasher factory for a serialization by file shards.
234
235        This is not recommended for BLAKE3 because it is not necessary. BLAKE3
236        already operates in parallel.
237
238        Args:
239            hashing_algorithm: The hashing algorithm to use to hash a shard.
240            chunk_size: The amount of file to read at once. Default is 1MB. A
241              special value of 0 signals to attempt to read everything in a
242              single call.
243            shard_size: The size of a file shard. Default is 1 GB.
244
245        Returns:
246            The hasher factory that should be used by the active serialization
247            method.
248        """
249
250        def _factory(
251            path: pathlib.Path, start: int, end: int
252        ) -> io.ShardedFileHasher:
253            hasher = self._build_stream_hasher(hashing_algorithm)
254            return io.ShardedFileHasher(
255                path,
256                hasher,
257                start=start,
258                end=end,
259                chunk_size=chunk_size,
260                shard_size=shard_size,
261            )
262
263        return _factory
264
265    def use_file_serialization(
266        self,
267        *,
268        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
269        chunk_size: int = 1048576,
270        max_workers: int | None = None,
271        allow_symlinks: bool = False,
272        ignore_paths: Iterable[pathlib.Path] = frozenset(),
273    ) -> Self:
274        """Configures serialization to build a manifest of (file, hash) pairs.
275
276        The serialization method in this configuration is changed to one where
277        every file in the model is paired with its digest and a manifest
278        containing all these pairings is being built.
279
280        Args:
281            hashing_algorithm: The hashing algorithm to use to hash a file.
282            chunk_size: The amount of file to read at once. Default is 1MB. A
283              special value of 0 signals to attempt to read everything in a
284              single call. Ignored for BLAKE3.
285            max_workers: Maximum number of workers to use in parallel. Default
286              is to defer to the `concurrent.futures` library to select the best
287              value for the current machine, or the number of logical cores
288              when doing BLAKE3 hashing. When reading files off of slower
289              hardware like an HDD rather than an SSD, and using BLAKE3,
290              setting max_workers to 1 may improve performance.
291            allow_symlinks: Controls whether symbolic links are included. If a
292              symlink is present but the flag is `False` (default) the
293              serialization would raise an error.
294            ignore_paths: Paths of files to ignore during serialization.
295
296        Returns:
297            The new hashing configuration with the new serialization method.
298        """
299        self._serializer = file.Serializer(
300            self._build_file_hasher_factory(
301                hashing_algorithm, chunk_size, max_workers
302            ),
303            max_workers=max_workers,
304            allow_symlinks=allow_symlinks,
305            ignore_paths=ignore_paths,
306        )
307        return self
308
309    def use_shard_serialization(
310        self,
311        *,
312        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
313        chunk_size: int = 1048576,
314        shard_size: int = 1_000_000_000,
315        max_workers: int | None = None,
316        allow_symlinks: bool = False,
317        ignore_paths: Iterable[pathlib.Path] = frozenset(),
318    ) -> Self:
319        """Configures serialization to build a manifest of (shard, hash) pairs.
320
321        For BLAKE3 this is equivalent to not sharding. Sharding is bypassed
322        because BLAKE3 already operates in parallel. This means the chunk_size
323        and shard_size args are ignored.
324
325        The serialization method in this configuration is changed to one where
326        every file in the model is sharded in equal sized shards, every shard is
327        paired with its digest and a manifest containing all these pairings is
328        being built.
329
330        Args:
331            hashing_algorithm: The hashing algorithm to use to hash a shard.
332            chunk_size: The amount of file to read at once. Default is 1MB. A
333              special value of 0 signals to attempt to read everything in a
334              single call.
335            shard_size: The size of a file shard. Default is 1 GB.
336            max_workers: Maximum number of workers to use in parallel. Default
337              is to defer to the `concurrent.futures` library to select the best
338              value for the current machine.
339            allow_symlinks: Controls whether symbolic links are included. If a
340              symlink is present but the flag is `False` (default) the
341              serialization would raise an error.
342            ignore_paths: Paths of files to ignore.
343
344        Returns:
345            The new hashing configuration with the new serialization method.
346        """
347        if hashing_algorithm == "blake3":
348            return self.use_file_serialization(
349                hashing_algorithm=hashing_algorithm,
350                chunk_size=chunk_size,
351                max_workers=max_workers,
352                allow_symlinks=allow_symlinks,
353                ignore_paths=ignore_paths,
354            )
355
356        self._serializer = file_shard.Serializer(
357            self._build_sharded_file_hasher_factory(
358                hashing_algorithm, chunk_size, shard_size
359            ),
360            max_workers=max_workers,
361            allow_symlinks=allow_symlinks,
362            ignore_paths=ignore_paths,
363        )
364        return self
365
366    def set_ignored_paths(
367        self, *, paths: Iterable[PathLike], ignore_git_paths: bool = True
368    ) -> Self:
369        """Configures the paths to be ignored during serialization of a model.
370
371        If the model is a single file, there are no paths that are ignored. If
372        the model is a directory, all paths are considered as relative to the
373        model directory, since we never look at files outside of it.
374
375        If an ignored path is a directory, serialization will ignore both the
376        path and any of its children.
377
378        Args:
379            paths: The paths to ignore.
380            ignore_git_paths: Whether to ignore git related paths (default) or
381              include them in the signature.
382
383        Returns:
384            The new hashing configuration with a new set of ignored paths.
385        """
386        # Preserve the user-provided relative paths; they are resolved against
387        # the model directory later when hashing.
388        self._ignored_paths = frozenset(pathlib.Path(p) for p in paths)
389        self._ignore_git_paths = ignore_git_paths
390        return self
391
392    def add_ignored_paths(
393        self, *, model_path: PathLike, paths: Iterable[PathLike]
394    ) -> None:
395        """Add more paths to ignore to existing set of paths.
396
397        Args:
398            model_path: The path to the model
399            paths: Additional paths to ignore. All path must be relative to
400                   the model directory.
401        """
402        newset = set(self._ignored_paths)
403        model_path = pathlib.Path(model_path)
404        for p in paths:
405            candidate = pathlib.Path(p)
406            full = model_path / candidate
407            try:
408                full.relative_to(model_path)
409            except ValueError:
410                continue
411            newset.add(candidate)
412        self._ignored_paths = newset
413
414    def set_allow_symlinks(self, allow_symlinks: bool) -> Self:
415        """Set whether following symlinks is allowed."""
416        self._allow_symlinks = allow_symlinks
417        return self
PathLike: TypeAlias = str | bytes | os.PathLike
def hash(model_path: str | bytes | os.PathLike) -> model_signing.manifest.Manifest:
72def hash(model_path: PathLike) -> manifest.Manifest:
73    """Hashes a model using the default configuration.
74
75    Hashing is the shared part between signing and verification and is also
76    expected to be the slowest component. When serializing a model, we need to
77    spend time proportional to the model size on disk.
78
79    This method returns a "manifest" of the model. A manifest is a collection of
80    every object in the model, paired with the corresponding hash. Currently, we
81    consider an object in the model to be either a file or a shard of the file.
82    Large models with large files will be hashed much faster when every shard is
83    hashed in parallel, at the cost of generating a larger payload for the
84    signature. In future releases we could support hashing individual tensors or
85    tensor slices for further speed optimizations for very large models.
86
87    Args:
88        model_path: The path to the model to hash.
89
90    Returns:
91        A manifest of the hashed model.
92    """
93    return Config().hash(model_path)

Hashes a model using the default configuration.

Hashing is the shared part between signing and verification and is also expected to be the slowest component. When serializing a model, we need to spend time proportional to the model size on disk.

This method returns a "manifest" of the model. A manifest is a collection of every object in the model, paired with the corresponding hash. Currently, we consider an object in the model to be either a file or a shard of the file. Large models with large files will be hashed much faster when every shard is hashed in parallel, at the cost of generating a larger payload for the signature. In future releases we could support hashing individual tensors or tensor slices for further speed optimizations for very large models.

Arguments:
  • model_path: The path to the model to hash.
Returns:

A manifest of the hashed model.

class Config:
 96class Config:
 97    """Configuration to use when hashing models.
 98
 99    Hashing is the shared part between signing and verification and is also
100    expected to be the slowest component. When serializing a model, we need to
101    spend time proportional to the model size on disk.
102
103    Hashing builds a "manifest" of the model. A manifest is a collection of
104    every object in the model, paired with the corresponding hash. Currently, we
105    consider an object in the model to be either a file or a shard of the file.
106    Large models with large files will be hashed much faster when every shard is
107    hashed in parallel, at the cost of generating a larger payload for the
108    signature. In future releases we could support hashing individual tensors or
109    tensor slices for further speed optimizations for very large models.
110
111    This configuration class supports configuring the hashing granularity. By
112    default, we hash at file level granularity.
113
114    This configuration class also supports configuring the hash method used to
115    generate the hash for every object in the model. We currently support
116    SHA256, BLAKE2, and BLAKE3, with SHA256 being the default.
117
118    This configuration class also supports configuring which paths from the
119    model directory should be ignored. These are files that doesn't impact the
120    behavior of the model, or files that won't be distributed with the model. By
121    default, only files that are associated with a git repository (`.git`,
122    `.gitattributes`, `.gitignore`, etc.) are ignored.
123    """
124
125    def __init__(self):
126        """Initializes the default configuration for hashing."""
127        self._ignored_paths = frozenset()
128        self._ignore_git_paths = True
129        self.use_file_serialization()
130        self._allow_symlinks = False
131
132    def hash(
133        self,
134        model_path: PathLike,
135        *,
136        files_to_hash: Iterable[PathLike] | None = None,
137    ) -> manifest.Manifest:
138        """Hashes a model using the current configuration."""
139        # All paths in ``_ignored_paths`` are expected to be relative to the
140        # model directory. Join them to ``model_path`` and ensure they do not
141        # escape it.
142        model_path = pathlib.Path(model_path)
143        ignored_paths = []
144        for p in self._ignored_paths:
145            full = model_path / p
146            try:
147                full.relative_to(model_path)
148            except ValueError:
149                continue
150            ignored_paths.append(full)
151
152        if self._ignore_git_paths:
153            ignored_paths.extend(
154                [
155                    model_path / p
156                    for p in [
157                        ".git/",
158                        ".gitattributes",
159                        ".github/",
160                        ".gitignore",
161                    ]
162                ]
163            )
164
165        self._serializer.set_allow_symlinks(self._allow_symlinks)
166
167        return self._serializer.serialize(
168            pathlib.Path(model_path),
169            ignore_paths=ignored_paths,
170            files_to_hash=files_to_hash,
171        )
172
173    def _build_stream_hasher(
174        self,
175        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
176    ) -> hashing.StreamingHashEngine:
177        """Builds a streaming hasher from a constant string.
178
179        Args:
180            hashing_algorithm: The hashing algorithm to use.
181
182        Returns:
183            An instance of the requested hasher.
184        """
185        match hashing_algorithm:
186            case "sha256":
187                return memory.SHA256()
188            case "blake2":
189                return memory.BLAKE2()
190            case "blake3":
191                return memory.BLAKE3()
192            case _:
193                raise ValueError(
194                    f"Unsupported hashing method {hashing_algorithm}"
195                )
196
197    def _build_file_hasher_factory(
198        self,
199        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
200        chunk_size: int = 1048576,
201        max_workers: int | None = None,
202    ) -> Callable[[pathlib.Path], io.FileHasher]:
203        """Builds the hasher factory for a serialization by file.
204
205        Args:
206            hashing_algorithm: The hashing algorithm to use to hash a file.
207            chunk_size: The amount of file to read at once. Default is 1MB. A
208              special value of 0 signals to attempt to read everything in a
209              single call. This is ignored for BLAKE3.
210            max_workers: Maximum number of workers to use in parallel. Defaults
211              to the number of logical cores. Only relevant for BLAKE3.
212
213        Returns:
214            The hasher factory that should be used by the active serialization
215            method.
216        """
217        if max_workers is None:
218            max_workers = blake3.blake3.AUTO
219
220        def _factory(path: pathlib.Path) -> io.FileHasher:
221            if hashing_algorithm == "blake3":
222                return io.Blake3FileHasher(path, max_threads=max_workers)
223            hasher = self._build_stream_hasher(hashing_algorithm)
224            return io.SimpleFileHasher(path, hasher, chunk_size=chunk_size)
225
226        return _factory
227
228    def _build_sharded_file_hasher_factory(
229        self,
230        hashing_algorithm: Literal["sha256", "blake2"] = "sha256",
231        chunk_size: int = 1048576,
232        shard_size: int = 1_000_000_000,
233    ) -> Callable[[pathlib.Path, int, int], io.ShardedFileHasher]:
234        """Builds the hasher factory for a serialization by file shards.
235
236        This is not recommended for BLAKE3 because it is not necessary. BLAKE3
237        already operates in parallel.
238
239        Args:
240            hashing_algorithm: The hashing algorithm to use to hash a shard.
241            chunk_size: The amount of file to read at once. Default is 1MB. A
242              special value of 0 signals to attempt to read everything in a
243              single call.
244            shard_size: The size of a file shard. Default is 1 GB.
245
246        Returns:
247            The hasher factory that should be used by the active serialization
248            method.
249        """
250
251        def _factory(
252            path: pathlib.Path, start: int, end: int
253        ) -> io.ShardedFileHasher:
254            hasher = self._build_stream_hasher(hashing_algorithm)
255            return io.ShardedFileHasher(
256                path,
257                hasher,
258                start=start,
259                end=end,
260                chunk_size=chunk_size,
261                shard_size=shard_size,
262            )
263
264        return _factory
265
266    def use_file_serialization(
267        self,
268        *,
269        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
270        chunk_size: int = 1048576,
271        max_workers: int | None = None,
272        allow_symlinks: bool = False,
273        ignore_paths: Iterable[pathlib.Path] = frozenset(),
274    ) -> Self:
275        """Configures serialization to build a manifest of (file, hash) pairs.
276
277        The serialization method in this configuration is changed to one where
278        every file in the model is paired with its digest and a manifest
279        containing all these pairings is being built.
280
281        Args:
282            hashing_algorithm: The hashing algorithm to use to hash a file.
283            chunk_size: The amount of file to read at once. Default is 1MB. A
284              special value of 0 signals to attempt to read everything in a
285              single call. Ignored for BLAKE3.
286            max_workers: Maximum number of workers to use in parallel. Default
287              is to defer to the `concurrent.futures` library to select the best
288              value for the current machine, or the number of logical cores
289              when doing BLAKE3 hashing. When reading files off of slower
290              hardware like an HDD rather than an SSD, and using BLAKE3,
291              setting max_workers to 1 may improve performance.
292            allow_symlinks: Controls whether symbolic links are included. If a
293              symlink is present but the flag is `False` (default) the
294              serialization would raise an error.
295            ignore_paths: Paths of files to ignore during serialization.
296
297        Returns:
298            The new hashing configuration with the new serialization method.
299        """
300        self._serializer = file.Serializer(
301            self._build_file_hasher_factory(
302                hashing_algorithm, chunk_size, max_workers
303            ),
304            max_workers=max_workers,
305            allow_symlinks=allow_symlinks,
306            ignore_paths=ignore_paths,
307        )
308        return self
309
310    def use_shard_serialization(
311        self,
312        *,
313        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
314        chunk_size: int = 1048576,
315        shard_size: int = 1_000_000_000,
316        max_workers: int | None = None,
317        allow_symlinks: bool = False,
318        ignore_paths: Iterable[pathlib.Path] = frozenset(),
319    ) -> Self:
320        """Configures serialization to build a manifest of (shard, hash) pairs.
321
322        For BLAKE3 this is equivalent to not sharding. Sharding is bypassed
323        because BLAKE3 already operates in parallel. This means the chunk_size
324        and shard_size args are ignored.
325
326        The serialization method in this configuration is changed to one where
327        every file in the model is sharded in equal sized shards, every shard is
328        paired with its digest and a manifest containing all these pairings is
329        being built.
330
331        Args:
332            hashing_algorithm: The hashing algorithm to use to hash a shard.
333            chunk_size: The amount of file to read at once. Default is 1MB. A
334              special value of 0 signals to attempt to read everything in a
335              single call.
336            shard_size: The size of a file shard. Default is 1 GB.
337            max_workers: Maximum number of workers to use in parallel. Default
338              is to defer to the `concurrent.futures` library to select the best
339              value for the current machine.
340            allow_symlinks: Controls whether symbolic links are included. If a
341              symlink is present but the flag is `False` (default) the
342              serialization would raise an error.
343            ignore_paths: Paths of files to ignore.
344
345        Returns:
346            The new hashing configuration with the new serialization method.
347        """
348        if hashing_algorithm == "blake3":
349            return self.use_file_serialization(
350                hashing_algorithm=hashing_algorithm,
351                chunk_size=chunk_size,
352                max_workers=max_workers,
353                allow_symlinks=allow_symlinks,
354                ignore_paths=ignore_paths,
355            )
356
357        self._serializer = file_shard.Serializer(
358            self._build_sharded_file_hasher_factory(
359                hashing_algorithm, chunk_size, shard_size
360            ),
361            max_workers=max_workers,
362            allow_symlinks=allow_symlinks,
363            ignore_paths=ignore_paths,
364        )
365        return self
366
367    def set_ignored_paths(
368        self, *, paths: Iterable[PathLike], ignore_git_paths: bool = True
369    ) -> Self:
370        """Configures the paths to be ignored during serialization of a model.
371
372        If the model is a single file, there are no paths that are ignored. If
373        the model is a directory, all paths are considered as relative to the
374        model directory, since we never look at files outside of it.
375
376        If an ignored path is a directory, serialization will ignore both the
377        path and any of its children.
378
379        Args:
380            paths: The paths to ignore.
381            ignore_git_paths: Whether to ignore git related paths (default) or
382              include them in the signature.
383
384        Returns:
385            The new hashing configuration with a new set of ignored paths.
386        """
387        # Preserve the user-provided relative paths; they are resolved against
388        # the model directory later when hashing.
389        self._ignored_paths = frozenset(pathlib.Path(p) for p in paths)
390        self._ignore_git_paths = ignore_git_paths
391        return self
392
393    def add_ignored_paths(
394        self, *, model_path: PathLike, paths: Iterable[PathLike]
395    ) -> None:
396        """Add more paths to ignore to existing set of paths.
397
398        Args:
399            model_path: The path to the model
400            paths: Additional paths to ignore. All path must be relative to
401                   the model directory.
402        """
403        newset = set(self._ignored_paths)
404        model_path = pathlib.Path(model_path)
405        for p in paths:
406            candidate = pathlib.Path(p)
407            full = model_path / candidate
408            try:
409                full.relative_to(model_path)
410            except ValueError:
411                continue
412            newset.add(candidate)
413        self._ignored_paths = newset
414
415    def set_allow_symlinks(self, allow_symlinks: bool) -> Self:
416        """Set whether following symlinks is allowed."""
417        self._allow_symlinks = allow_symlinks
418        return self

Configuration to use when hashing models.

Hashing is the shared part between signing and verification and is also expected to be the slowest component. When serializing a model, we need to spend time proportional to the model size on disk.

Hashing builds a "manifest" of the model. A manifest is a collection of every object in the model, paired with the corresponding hash. Currently, we consider an object in the model to be either a file or a shard of the file. Large models with large files will be hashed much faster when every shard is hashed in parallel, at the cost of generating a larger payload for the signature. In future releases we could support hashing individual tensors or tensor slices for further speed optimizations for very large models.

This configuration class supports configuring the hashing granularity. By default, we hash at file level granularity.

This configuration class also supports configuring the hash method used to generate the hash for every object in the model. We currently support SHA256, BLAKE2, and BLAKE3, with SHA256 being the default.

This configuration class also supports configuring which paths from the model directory should be ignored. These are files that doesn't impact the behavior of the model, or files that won't be distributed with the model. By default, only files that are associated with a git repository (.git, .gitattributes, .gitignore, etc.) are ignored.

Config()
125    def __init__(self):
126        """Initializes the default configuration for hashing."""
127        self._ignored_paths = frozenset()
128        self._ignore_git_paths = True
129        self.use_file_serialization()
130        self._allow_symlinks = False

Initializes the default configuration for hashing.

def hash( self, model_path: str | bytes | os.PathLike, *, files_to_hash: Iterable[str | bytes | os.PathLike] | None = None) -> model_signing.manifest.Manifest:
132    def hash(
133        self,
134        model_path: PathLike,
135        *,
136        files_to_hash: Iterable[PathLike] | None = None,
137    ) -> manifest.Manifest:
138        """Hashes a model using the current configuration."""
139        # All paths in ``_ignored_paths`` are expected to be relative to the
140        # model directory. Join them to ``model_path`` and ensure they do not
141        # escape it.
142        model_path = pathlib.Path(model_path)
143        ignored_paths = []
144        for p in self._ignored_paths:
145            full = model_path / p
146            try:
147                full.relative_to(model_path)
148            except ValueError:
149                continue
150            ignored_paths.append(full)
151
152        if self._ignore_git_paths:
153            ignored_paths.extend(
154                [
155                    model_path / p
156                    for p in [
157                        ".git/",
158                        ".gitattributes",
159                        ".github/",
160                        ".gitignore",
161                    ]
162                ]
163            )
164
165        self._serializer.set_allow_symlinks(self._allow_symlinks)
166
167        return self._serializer.serialize(
168            pathlib.Path(model_path),
169            ignore_paths=ignored_paths,
170            files_to_hash=files_to_hash,
171        )

Hashes a model using the current configuration.

def use_file_serialization( self, *, hashing_algorithm: Literal['sha256', 'blake2', 'blake3'] = 'sha256', chunk_size: int = 1048576, max_workers: int | None = None, allow_symlinks: bool = False, ignore_paths: Iterable[pathlib.Path] = frozenset()) -> Self:
266    def use_file_serialization(
267        self,
268        *,
269        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
270        chunk_size: int = 1048576,
271        max_workers: int | None = None,
272        allow_symlinks: bool = False,
273        ignore_paths: Iterable[pathlib.Path] = frozenset(),
274    ) -> Self:
275        """Configures serialization to build a manifest of (file, hash) pairs.
276
277        The serialization method in this configuration is changed to one where
278        every file in the model is paired with its digest and a manifest
279        containing all these pairings is being built.
280
281        Args:
282            hashing_algorithm: The hashing algorithm to use to hash a file.
283            chunk_size: The amount of file to read at once. Default is 1MB. A
284              special value of 0 signals to attempt to read everything in a
285              single call. Ignored for BLAKE3.
286            max_workers: Maximum number of workers to use in parallel. Default
287              is to defer to the `concurrent.futures` library to select the best
288              value for the current machine, or the number of logical cores
289              when doing BLAKE3 hashing. When reading files off of slower
290              hardware like an HDD rather than an SSD, and using BLAKE3,
291              setting max_workers to 1 may improve performance.
292            allow_symlinks: Controls whether symbolic links are included. If a
293              symlink is present but the flag is `False` (default) the
294              serialization would raise an error.
295            ignore_paths: Paths of files to ignore during serialization.
296
297        Returns:
298            The new hashing configuration with the new serialization method.
299        """
300        self._serializer = file.Serializer(
301            self._build_file_hasher_factory(
302                hashing_algorithm, chunk_size, max_workers
303            ),
304            max_workers=max_workers,
305            allow_symlinks=allow_symlinks,
306            ignore_paths=ignore_paths,
307        )
308        return self

Configures serialization to build a manifest of (file, hash) pairs.

The serialization method in this configuration is changed to one where every file in the model is paired with its digest and a manifest containing all these pairings is being built.

Arguments:
  • hashing_algorithm: The hashing algorithm to use to hash a file.
  • chunk_size: The amount of file to read at once. Default is 1MB. A special value of 0 signals to attempt to read everything in a single call. Ignored for BLAKE3.
  • max_workers: Maximum number of workers to use in parallel. Default is to defer to the concurrent.futures library to select the best value for the current machine, or the number of logical cores when doing BLAKE3 hashing. When reading files off of slower hardware like an HDD rather than an SSD, and using BLAKE3, setting max_workers to 1 may improve performance.
  • allow_symlinks: Controls whether symbolic links are included. If a symlink is present but the flag is False (default) the serialization would raise an error.
  • ignore_paths: Paths of files to ignore during serialization.
Returns:

The new hashing configuration with the new serialization method.

def use_shard_serialization( self, *, hashing_algorithm: Literal['sha256', 'blake2', 'blake3'] = 'sha256', chunk_size: int = 1048576, shard_size: int = 1000000000, max_workers: int | None = None, allow_symlinks: bool = False, ignore_paths: Iterable[pathlib.Path] = frozenset()) -> Self:
310    def use_shard_serialization(
311        self,
312        *,
313        hashing_algorithm: Literal["sha256", "blake2", "blake3"] = "sha256",
314        chunk_size: int = 1048576,
315        shard_size: int = 1_000_000_000,
316        max_workers: int | None = None,
317        allow_symlinks: bool = False,
318        ignore_paths: Iterable[pathlib.Path] = frozenset(),
319    ) -> Self:
320        """Configures serialization to build a manifest of (shard, hash) pairs.
321
322        For BLAKE3 this is equivalent to not sharding. Sharding is bypassed
323        because BLAKE3 already operates in parallel. This means the chunk_size
324        and shard_size args are ignored.
325
326        The serialization method in this configuration is changed to one where
327        every file in the model is sharded in equal sized shards, every shard is
328        paired with its digest and a manifest containing all these pairings is
329        being built.
330
331        Args:
332            hashing_algorithm: The hashing algorithm to use to hash a shard.
333            chunk_size: The amount of file to read at once. Default is 1MB. A
334              special value of 0 signals to attempt to read everything in a
335              single call.
336            shard_size: The size of a file shard. Default is 1 GB.
337            max_workers: Maximum number of workers to use in parallel. Default
338              is to defer to the `concurrent.futures` library to select the best
339              value for the current machine.
340            allow_symlinks: Controls whether symbolic links are included. If a
341              symlink is present but the flag is `False` (default) the
342              serialization would raise an error.
343            ignore_paths: Paths of files to ignore.
344
345        Returns:
346            The new hashing configuration with the new serialization method.
347        """
348        if hashing_algorithm == "blake3":
349            return self.use_file_serialization(
350                hashing_algorithm=hashing_algorithm,
351                chunk_size=chunk_size,
352                max_workers=max_workers,
353                allow_symlinks=allow_symlinks,
354                ignore_paths=ignore_paths,
355            )
356
357        self._serializer = file_shard.Serializer(
358            self._build_sharded_file_hasher_factory(
359                hashing_algorithm, chunk_size, shard_size
360            ),
361            max_workers=max_workers,
362            allow_symlinks=allow_symlinks,
363            ignore_paths=ignore_paths,
364        )
365        return self

Configures serialization to build a manifest of (shard, hash) pairs.

For BLAKE3 this is equivalent to not sharding. Sharding is bypassed because BLAKE3 already operates in parallel. This means the chunk_size and shard_size args are ignored.

The serialization method in this configuration is changed to one where every file in the model is sharded in equal sized shards, every shard is paired with its digest and a manifest containing all these pairings is being built.

Arguments:
  • hashing_algorithm: The hashing algorithm to use to hash a shard.
  • chunk_size: The amount of file to read at once. Default is 1MB. A special value of 0 signals to attempt to read everything in a single call.
  • shard_size: The size of a file shard. Default is 1 GB.
  • max_workers: Maximum number of workers to use in parallel. Default is to defer to the concurrent.futures library to select the best value for the current machine.
  • allow_symlinks: Controls whether symbolic links are included. If a symlink is present but the flag is False (default) the serialization would raise an error.
  • ignore_paths: Paths of files to ignore.
Returns:

The new hashing configuration with the new serialization method.

def set_ignored_paths( self, *, paths: Iterable[str | bytes | os.PathLike], ignore_git_paths: bool = True) -> Self:
367    def set_ignored_paths(
368        self, *, paths: Iterable[PathLike], ignore_git_paths: bool = True
369    ) -> Self:
370        """Configures the paths to be ignored during serialization of a model.
371
372        If the model is a single file, there are no paths that are ignored. If
373        the model is a directory, all paths are considered as relative to the
374        model directory, since we never look at files outside of it.
375
376        If an ignored path is a directory, serialization will ignore both the
377        path and any of its children.
378
379        Args:
380            paths: The paths to ignore.
381            ignore_git_paths: Whether to ignore git related paths (default) or
382              include them in the signature.
383
384        Returns:
385            The new hashing configuration with a new set of ignored paths.
386        """
387        # Preserve the user-provided relative paths; they are resolved against
388        # the model directory later when hashing.
389        self._ignored_paths = frozenset(pathlib.Path(p) for p in paths)
390        self._ignore_git_paths = ignore_git_paths
391        return self

Configures the paths to be ignored during serialization of a model.

If the model is a single file, there are no paths that are ignored. If the model is a directory, all paths are considered as relative to the model directory, since we never look at files outside of it.

If an ignored path is a directory, serialization will ignore both the path and any of its children.

Arguments:
  • paths: The paths to ignore.
  • ignore_git_paths: Whether to ignore git related paths (default) or include them in the signature.
Returns:

The new hashing configuration with a new set of ignored paths.

def add_ignored_paths( self, *, model_path: str | bytes | os.PathLike, paths: Iterable[str | bytes | os.PathLike]) -> None:
393    def add_ignored_paths(
394        self, *, model_path: PathLike, paths: Iterable[PathLike]
395    ) -> None:
396        """Add more paths to ignore to existing set of paths.
397
398        Args:
399            model_path: The path to the model
400            paths: Additional paths to ignore. All path must be relative to
401                   the model directory.
402        """
403        newset = set(self._ignored_paths)
404        model_path = pathlib.Path(model_path)
405        for p in paths:
406            candidate = pathlib.Path(p)
407            full = model_path / candidate
408            try:
409                full.relative_to(model_path)
410            except ValueError:
411                continue
412            newset.add(candidate)
413        self._ignored_paths = newset

Add more paths to ignore to existing set of paths.

Arguments:
  • model_path: The path to the model
  • paths: Additional paths to ignore. All path must be relative to the model directory.