vaezip 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (44) hide show
  1. {vaezip-0.1.0/src/vaezip.egg-info → vaezip-0.2.0}/PKG-INFO +28 -11
  2. {vaezip-0.1.0 → vaezip-0.2.0}/README.md +27 -10
  3. {vaezip-0.1.0 → vaezip-0.2.0}/pyproject.toml +1 -1
  4. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/__init__.py +6 -4
  5. vaezip-0.2.0/src/vaezip/api.py +188 -0
  6. vaezip-0.2.0/src/vaezip/encoded_tensor.py +93 -0
  7. vaezip-0.2.0/src/vaezip/serialization.py +189 -0
  8. {vaezip-0.1.0 → vaezip-0.2.0/src/vaezip.egg-info}/PKG-INFO +28 -11
  9. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip.egg-info/SOURCES.txt +2 -0
  10. vaezip-0.2.0/tests/test_api.py +237 -0
  11. vaezip-0.2.0/tests/test_encoded_tensor.py +111 -0
  12. vaezip-0.2.0/tests/test_serialization.py +181 -0
  13. vaezip-0.1.0/src/vaezip/api.py +0 -139
  14. vaezip-0.1.0/src/vaezip/encoded_tensor.py +0 -132
  15. vaezip-0.1.0/tests/test_api.py +0 -142
  16. vaezip-0.1.0/tests/test_encoded_tensor.py +0 -96
  17. {vaezip-0.1.0 → vaezip-0.2.0}/LICENSE +0 -0
  18. {vaezip-0.1.0 → vaezip-0.2.0}/setup.cfg +0 -0
  19. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/_version.py +0 -0
  20. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/__init__.py +0 -0
  21. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/auto.py +0 -0
  22. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/base.py +0 -0
  23. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/block_range_quant.py +0 -0
  24. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/cast.py +0 -0
  25. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/config.py +0 -0
  26. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/lloyd_block_range_quant.py +0 -0
  27. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/range_quant.py +0 -0
  28. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/shortcuts.py +0 -0
  29. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/py.typed +0 -0
  30. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/__init__.py +0 -0
  31. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/bit_packing.py +0 -0
  32. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/blocks.py +0 -0
  33. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/layout.py +0 -0
  34. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/lloyd.py +0 -0
  35. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip.egg-info/dependency_links.txt +0 -0
  36. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip.egg-info/requires.txt +0 -0
  37. {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip.egg-info/top_level.txt +0 -0
  38. {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_block_range_quant_codec.py +0 -0
  39. {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_cast_codec.py +0 -0
  40. {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_config.py +0 -0
  41. {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_lloyd_block_range_quant_codec.py +0 -0
  42. {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_package.py +0 -0
  43. {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_range_quant_codec.py +0 -0
  44. {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vaezip
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Compression of VAE latents: low-bit quantization codecs on PyTorch and safetensors
5
5
  License-Expression: Unlicense
6
6
  Project-URL: Homepage, https://github.com/tensorforger/VaeZip
@@ -62,12 +62,30 @@ import vaezip
62
62
 
63
63
  latents = torch.randn([4, 32, 128, 128], device="cuda")
64
64
 
65
- vaezip.save(latents, "latents.vz.safetensors", codec="lbrq_4bit")
66
- restored = vaezip.load("latents.vz.safetensors", device="cuda")
65
+ vaezip.save_tensor(latents, "latents.vz.safetensors", codec="lbrq_4bit")
66
+ restored = vaezip.load_tensor("latents.vz.safetensors", device="cuda")
67
67
  ```
68
68
 
69
- The codec config is stored in the file metadata, so `load()` needs no codec.
70
- Pass `save_metadata=False` to drop it, then `load()` requires an explicit `codec=`.
69
+ The codec config is stored in the file, so `load_tensor()` needs no codec.
70
+
71
+ Several tensors, each with its own codec, go to one file as a dict of `EncodedTensor`:
72
+
73
+ ```python
74
+ from vaezip.codecs import AutoCodec
75
+
76
+ tensor_a = torch.randn([4, 32, 128, 128])
77
+ tensor_b = torch.randn([4, 64, 256])
78
+ codec_b = AutoCodec.from_config("brq_8bit", block_size=(4,))
79
+
80
+ batch_dict = {
81
+ "tensor_a": vaezip.encode(tensor_a, codec="lbrq_3bit"),
82
+ "tensor_b": vaezip.encode(tensor_b, codec=codec_b),
83
+ }
84
+ vaezip.save(batch_dict, "batch_dict.vz.safetensors")
85
+
86
+ batch_dict = vaezip.load("batch_dict.vz.safetensors")
87
+ tensor_a = batch_dict["tensor_a"].decode(device="cuda")
88
+ ```
71
89
 
72
90
  ## Key concepts
73
91
 
@@ -86,13 +104,12 @@ restored = codec.decode(encoded)
86
104
  ```
87
105
 
88
106
  **EncodedTensor** holds named tensors plus string metadata with the codec config.
89
- It serializes to `.safetensors` (default) or `.pth`:
107
+ `encoded.decode()` restores the tensor with the codec from the metadata, or with an explicit `codec=`.
108
+ `vaezip.encode()` and `vaezip.decode()` are the same with codec shortcuts and configs.
90
109
 
91
- ```python
92
- data = encoded.serialize() # or vaezip.serialize(encoded)
93
- encoded = vaezip.deserialize(data)
94
- data = encoded.serialize(save_metadata=False, format="pth")
95
- ```
110
+ **Files** hold a dict of `EncodedTensor`, `vaezip.save()` and `vaezip.load()` work with paths or bytes.
111
+ The format is `.safetensors` by default, `.pth` only with `format="pth"`.
112
+ `vaezip.save_tensor()` and `vaezip.load_tensor()` are shortcuts for a file with a single tensor stored as `"tensor"`.
96
113
 
97
114
  **Configs** are diffusers-style JSON files and can be created from files, shortcut names or dicts.
98
115
  Keyword arguments override config values:
@@ -33,12 +33,30 @@ import vaezip
33
33
 
34
34
  latents = torch.randn([4, 32, 128, 128], device="cuda")
35
35
 
36
- vaezip.save(latents, "latents.vz.safetensors", codec="lbrq_4bit")
37
- restored = vaezip.load("latents.vz.safetensors", device="cuda")
36
+ vaezip.save_tensor(latents, "latents.vz.safetensors", codec="lbrq_4bit")
37
+ restored = vaezip.load_tensor("latents.vz.safetensors", device="cuda")
38
38
  ```
39
39
 
40
- The codec config is stored in the file metadata, so `load()` needs no codec.
41
- Pass `save_metadata=False` to drop it, then `load()` requires an explicit `codec=`.
40
+ The codec config is stored in the file, so `load_tensor()` needs no codec.
41
+
42
+ Several tensors, each with its own codec, go to one file as a dict of `EncodedTensor`:
43
+
44
+ ```python
45
+ from vaezip.codecs import AutoCodec
46
+
47
+ tensor_a = torch.randn([4, 32, 128, 128])
48
+ tensor_b = torch.randn([4, 64, 256])
49
+ codec_b = AutoCodec.from_config("brq_8bit", block_size=(4,))
50
+
51
+ batch_dict = {
52
+ "tensor_a": vaezip.encode(tensor_a, codec="lbrq_3bit"),
53
+ "tensor_b": vaezip.encode(tensor_b, codec=codec_b),
54
+ }
55
+ vaezip.save(batch_dict, "batch_dict.vz.safetensors")
56
+
57
+ batch_dict = vaezip.load("batch_dict.vz.safetensors")
58
+ tensor_a = batch_dict["tensor_a"].decode(device="cuda")
59
+ ```
42
60
 
43
61
  ## Key concepts
44
62
 
@@ -57,13 +75,12 @@ restored = codec.decode(encoded)
57
75
  ```
58
76
 
59
77
  **EncodedTensor** holds named tensors plus string metadata with the codec config.
60
- It serializes to `.safetensors` (default) or `.pth`:
78
+ `encoded.decode()` restores the tensor with the codec from the metadata, or with an explicit `codec=`.
79
+ `vaezip.encode()` and `vaezip.decode()` are the same with codec shortcuts and configs.
61
80
 
62
- ```python
63
- data = encoded.serialize() # or vaezip.serialize(encoded)
64
- encoded = vaezip.deserialize(data)
65
- data = encoded.serialize(save_metadata=False, format="pth")
66
- ```
81
+ **Files** hold a dict of `EncodedTensor`, `vaezip.save()` and `vaezip.load()` work with paths or bytes.
82
+ The format is `.safetensors` by default, `.pth` only with `format="pth"`.
83
+ `vaezip.save_tensor()` and `vaezip.load_tensor()` are shortcuts for a file with a single tensor stored as `"tensor"`.
67
84
 
68
85
  **Configs** are diffusers-style JSON files and can be created from files, shortcut names or dicts.
69
86
  Keyword arguments override config values:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vaezip"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Compression of VAE latents: low-bit quantization codecs on PyTorch and safetensors"
9
9
  readme = "README.md"
10
10
  license = "Unlicense"
@@ -2,15 +2,17 @@
2
2
 
3
3
  from vaezip import codecs
4
4
  from vaezip._version import __version__
5
- from vaezip.api import load, save
6
- from vaezip.encoded_tensor import EncodedTensor, deserialize, serialize
5
+ from vaezip.api import decode, encode, load, load_tensor, save, save_tensor
6
+ from vaezip.encoded_tensor import EncodedTensor
7
7
 
8
8
  __all__ = [
9
9
  "EncodedTensor",
10
10
  "__version__",
11
11
  "codecs",
12
- "deserialize",
12
+ "decode",
13
+ "encode",
13
14
  "load",
15
+ "load_tensor",
14
16
  "save",
15
- "serialize",
17
+ "save_tensor",
16
18
  ]
@@ -0,0 +1,188 @@
1
+ """High level encoding, decoding, saving and loading of tensors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import overload
8
+
9
+ import torch
10
+
11
+ from vaezip.codecs.auto import AutoCodec
12
+ from vaezip.codecs.base import Codec
13
+ from vaezip.codecs.shortcuts import DEFAULT_SHORTCUT
14
+ from vaezip.encoded_tensor import EncodedTensor
15
+ from vaezip.serialization import (
16
+ SAFETENSORS_FORMAT,
17
+ SerializationFormat,
18
+ deserialize,
19
+ serialize,
20
+ )
21
+
22
+ CodecSpecification = Codec | str | os.PathLike[str]
23
+ SINGLE_TENSOR_NAME = "tensor"
24
+
25
+
26
+ def encode(
27
+ tensor: torch.Tensor, codec: CodecSpecification | None = None
28
+ ) -> EncodedTensor:
29
+ """
30
+ args:
31
+ tensor: [B, ...], floating point
32
+ codec: codec object, shortcut name or config path, default is "cast_fp32"
33
+ returns:
34
+ encoded: tensors on the device of the input, metadata with the codec config
35
+ """
36
+ if codec is None:
37
+ codec = DEFAULT_SHORTCUT
38
+ resolved_codec = codec if isinstance(codec, Codec) else AutoCodec.from_config(codec)
39
+ return resolved_codec.encode(tensor)
40
+
41
+
42
+ def decode(
43
+ encoded: EncodedTensor,
44
+ codec: CodecSpecification | None = None,
45
+ dtype: torch.dtype = torch.float32,
46
+ device: torch.device | str | None = None,
47
+ ) -> torch.Tensor:
48
+ """
49
+ Same as `EncodedTensor.decode()`.
50
+
51
+ returns:
52
+ tensor: [B, ...], original shape, dtype: dtype
53
+ """
54
+ return encoded.decode(codec=codec, dtype=dtype, device=device)
55
+
56
+
57
+ @overload
58
+ def save(
59
+ encoded_tensors: dict[str, EncodedTensor],
60
+ path: None = None,
61
+ format: SerializationFormat = SAFETENSORS_FORMAT,
62
+ ) -> bytes: ...
63
+
64
+
65
+ @overload
66
+ def save(
67
+ encoded_tensors: dict[str, EncodedTensor],
68
+ path: str | os.PathLike[str],
69
+ format: SerializationFormat = SAFETENSORS_FORMAT,
70
+ ) -> None: ...
71
+
72
+
73
+ def save(
74
+ encoded_tensors: dict[str, EncodedTensor],
75
+ path: str | os.PathLike[str] | None = None,
76
+ format: SerializationFormat = SAFETENSORS_FORMAT,
77
+ ) -> bytes | None:
78
+ """
79
+ Writes named encoded tensors to a file, or returns bytes if there is no path.
80
+
81
+ args:
82
+ encoded_tensors: name -> encoded tensor, tensors may use different codecs
83
+ path: output file, `None` to return bytes
84
+ format: "safetensors" or "pth", never inferred from the path
85
+ """
86
+ return _write_or_return(serialize(encoded_tensors, format=format), path)
87
+
88
+
89
+ def load(
90
+ source: str | os.PathLike[str] | bytes | bytearray,
91
+ device: torch.device | str | None = None,
92
+ format: SerializationFormat = SAFETENSORS_FORMAT,
93
+ ) -> dict[str, EncodedTensor]:
94
+ """
95
+ Reads a file or bytes written by `save()`.
96
+
97
+ args:
98
+ source: file path or bytes
99
+ device: device of the loaded tensors, default is CPU
100
+ format: "safetensors" or "pth", never inferred from the path
101
+ returns:
102
+ encoded_tensors: name -> encoded tensor, sorted by name
103
+ """
104
+ if isinstance(source, (bytes, bytearray)):
105
+ data = bytes(source)
106
+ else:
107
+ data = Path(source).read_bytes()
108
+ encoded_tensors = deserialize(data, format=format)
109
+ if device is not None:
110
+ encoded_tensors = {
111
+ name: encoded.to(device) for name, encoded in encoded_tensors.items()
112
+ }
113
+ return encoded_tensors
114
+
115
+
116
+ @overload
117
+ def save_tensor(
118
+ tensor: torch.Tensor,
119
+ path: None = None,
120
+ codec: CodecSpecification | None = None,
121
+ format: SerializationFormat = SAFETENSORS_FORMAT,
122
+ ) -> bytes: ...
123
+
124
+
125
+ @overload
126
+ def save_tensor(
127
+ tensor: torch.Tensor,
128
+ path: str | os.PathLike[str],
129
+ codec: CodecSpecification | None = None,
130
+ format: SerializationFormat = SAFETENSORS_FORMAT,
131
+ ) -> None: ...
132
+
133
+
134
+ def save_tensor(
135
+ tensor: torch.Tensor,
136
+ path: str | os.PathLike[str] | None = None,
137
+ codec: CodecSpecification | None = None,
138
+ format: SerializationFormat = SAFETENSORS_FORMAT,
139
+ ) -> bytes | None:
140
+ """
141
+ Encodes a single tensor and saves it as `{"tensor": encoded}`.
142
+
143
+ args:
144
+ tensor: [B, ...], floating point
145
+ path: output file, `None` to return bytes
146
+ codec: codec object, shortcut name or config path, default is "cast_fp32"
147
+ format: "safetensors" or "pth", never inferred from the path
148
+ """
149
+ encoded_tensors = {SINGLE_TENSOR_NAME: encode(tensor, codec=codec)}
150
+ return _write_or_return(serialize(encoded_tensors, format=format), path)
151
+
152
+
153
+ def load_tensor(
154
+ source: str | os.PathLike[str] | bytes | bytearray,
155
+ codec: CodecSpecification | None = None,
156
+ dtype: torch.dtype = torch.float32,
157
+ device: torch.device | str | None = None,
158
+ format: SerializationFormat = SAFETENSORS_FORMAT,
159
+ ) -> torch.Tensor:
160
+ """
161
+ Loads and decodes a file that holds exactly one entry named "tensor".
162
+
163
+ args:
164
+ source: file path or bytes
165
+ codec: codec object, shortcut name or config path, by default the codec
166
+ from the metadata
167
+ dtype: floating point dtype of the result
168
+ device: device to decode on, default is CPU
169
+ format: "safetensors" or "pth", never inferred from the path
170
+ returns:
171
+ tensor: [B, ...], original shape, dtype: dtype
172
+ """
173
+ encoded_tensors = load(source, format=format)
174
+ if list(encoded_tensors) != [SINGLE_TENSOR_NAME]:
175
+ raise ValueError(
176
+ f"Expected a single entry named {SINGLE_TENSOR_NAME!r}, "
177
+ f"found: {list(encoded_tensors)}"
178
+ )
179
+ return encoded_tensors[SINGLE_TENSOR_NAME].decode(
180
+ codec=codec, dtype=dtype, device=device
181
+ )
182
+
183
+
184
+ def _write_or_return(data: bytes, path: str | os.PathLike[str] | None) -> bytes | None:
185
+ if path is None:
186
+ return data
187
+ Path(path).write_bytes(data)
188
+ return None
@@ -0,0 +1,93 @@
1
+ """Container for compressed latents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ import torch
11
+
12
+ if TYPE_CHECKING:
13
+ from vaezip.codecs.base import Codec
14
+
15
+ CODEC_CONFIG_KEY = "codec_config"
16
+
17
+
18
+ @dataclass
19
+ class EncodedTensor:
20
+ """
21
+ Output of `Codec.encode()` and input of `Codec.decode()`.
22
+
23
+ tensors: named tensors with the compressed representation.
24
+ metadata: optional string metadata, normally holds the JSON config of the codec.
25
+ """
26
+
27
+ tensors: dict[str, torch.Tensor]
28
+ metadata: dict[str, str] = field(default_factory=dict)
29
+
30
+ @property
31
+ def codec_config(self) -> dict[str, Any] | None:
32
+ """Parsed codec config from the metadata, `None` if there is none."""
33
+ raw_config = self.metadata.get(CODEC_CONFIG_KEY)
34
+ if raw_config is None:
35
+ return None
36
+ config = json.loads(raw_config)
37
+ if not isinstance(config, dict):
38
+ raise ValueError("Codec config in the metadata must be a JSON object")
39
+ return config
40
+
41
+ @property
42
+ def nbytes(self) -> int:
43
+ """Total size of the stored tensors in bytes."""
44
+ return sum(
45
+ tensor.numel() * tensor.element_size() for tensor in self.tensors.values()
46
+ )
47
+
48
+ def to(self, device: torch.device | str) -> EncodedTensor:
49
+ return EncodedTensor(
50
+ tensors={name: tensor.to(device) for name, tensor in self.tensors.items()},
51
+ metadata=dict(self.metadata),
52
+ )
53
+
54
+ def decode(
55
+ self,
56
+ codec: Codec | str | os.PathLike[str] | None = None,
57
+ dtype: torch.dtype = torch.float32,
58
+ device: torch.device | str | None = None,
59
+ ) -> torch.Tensor:
60
+ """
61
+ args:
62
+ codec: codec object, shortcut name or config path, by default the codec
63
+ from the metadata. If both are available they must have the same config.
64
+ dtype: floating point dtype of the result
65
+ device: device to decode on, by default the device of the stored tensors
66
+ returns:
67
+ tensor: [B, ...], original shape, dtype: dtype
68
+ """
69
+ from vaezip.codecs.auto import AutoCodec
70
+ from vaezip.codecs.base import Codec
71
+ from vaezip.codecs.config import without_version
72
+
73
+ stored_config = self.codec_config
74
+ if codec is None:
75
+ if stored_config is None:
76
+ raise ValueError(
77
+ "The encoded tensor has no codec config in its metadata, "
78
+ "pass the codec explicitly"
79
+ )
80
+ resolved_codec = AutoCodec.from_config(stored_config)
81
+ else:
82
+ resolved_codec = (
83
+ codec if isinstance(codec, Codec) else AutoCodec.from_config(codec)
84
+ )
85
+ if stored_config is not None and without_version(
86
+ stored_config
87
+ ) != without_version(resolved_codec.get_config()):
88
+ raise ValueError(
89
+ f"The codec {resolved_codec!r} does not match the config stored "
90
+ f"in the encoded tensor: {stored_config}"
91
+ )
92
+ encoded = self if device is None else self.to(device)
93
+ return resolved_codec.decode(encoded, dtype=dtype)
@@ -0,0 +1,189 @@
1
+ """Storage of named encoded tensors in safetensors and pth containers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import json
7
+ import pickle
8
+ import struct
9
+ from typing import Any, Literal, get_args
10
+
11
+ import torch
12
+ from safetensors import SafetensorError
13
+ from safetensors.torch import load as load_safetensors
14
+ from safetensors.torch import save as save_safetensors
15
+
16
+ from vaezip._version import __version__
17
+ from vaezip.encoded_tensor import EncodedTensor
18
+
19
+ SerializationFormat = Literal["safetensors", "pth"]
20
+ SAFETENSORS_FORMAT: SerializationFormat = "safetensors"
21
+ PTH_FORMAT: SerializationFormat = "pth"
22
+ SUPPORTED_FORMATS: tuple[str, ...] = get_args(SerializationFormat)
23
+
24
+ FORMAT_VERSION = "1"
25
+ FORMAT_VERSION_KEY = "vaezip_format_version"
26
+ PACKAGE_VERSION_KEY = "vaezip_version"
27
+ KEY_DELIMITER = "."
28
+
29
+ _PTH_TENSORS_KEY = "tensors"
30
+ _PTH_METADATA_KEY = "metadata"
31
+ _SAFETENSORS_HEADER_SIZE_BYTES = 8
32
+ _SAFETENSORS_METADATA_KEY = "__metadata__"
33
+
34
+
35
+ def check_format(format: str) -> None:
36
+ if format not in SUPPORTED_FORMATS:
37
+ raise ValueError(
38
+ f"Unsupported format {format!r}, expected one of {SUPPORTED_FORMATS}"
39
+ )
40
+
41
+
42
+ def flatten(
43
+ encoded_tensors: dict[str, EncodedTensor],
44
+ ) -> tuple[dict[str, torch.Tensor], dict[str, str]]:
45
+ """
46
+ Joins entry names with inner names: tensor "codes" of entry "latents" becomes
47
+ "latents.codes", metadata is flattened the same way.
48
+
49
+ args:
50
+ encoded_tensors: entry name -> encoded tensor, names are non-empty
51
+ returns:
52
+ tensors: flat name -> tensor, on CPU, contiguous
53
+ metadata: flat key -> string, includes the format and package versions
54
+ """
55
+ if not isinstance(encoded_tensors, dict):
56
+ raise TypeError(
57
+ f"Expected a dict of EncodedTensor, got {type(encoded_tensors).__name__}"
58
+ )
59
+ if not encoded_tensors:
60
+ raise ValueError("Can not save an empty dict")
61
+ tensors: dict[str, torch.Tensor] = {}
62
+ metadata: dict[str, str] = {
63
+ FORMAT_VERSION_KEY: FORMAT_VERSION,
64
+ PACKAGE_VERSION_KEY: __version__,
65
+ }
66
+ for name, encoded in encoded_tensors.items():
67
+ if not isinstance(name, str) or not name:
68
+ raise ValueError(f"Names must be non-empty strings, got {name!r}")
69
+ if not isinstance(encoded, EncodedTensor):
70
+ raise TypeError(
71
+ f"Expected an EncodedTensor for {name!r}, got {type(encoded).__name__}"
72
+ )
73
+ if not encoded.tensors:
74
+ raise ValueError(f"EncodedTensor {name!r} has no tensors")
75
+ for inner_name, tensor in encoded.tensors.items():
76
+ _check_inner_key(name, inner_name)
77
+ tensors[f"{name}{KEY_DELIMITER}{inner_name}"] = (
78
+ tensor.detach().to("cpu", copy=True).contiguous()
79
+ )
80
+ for key, value in encoded.metadata.items():
81
+ _check_inner_key(name, key)
82
+ if not isinstance(value, str):
83
+ raise ValueError(
84
+ f"Metadata {key!r} of {name!r} must be a string, "
85
+ f"got {type(value).__name__}"
86
+ )
87
+ metadata[f"{name}{KEY_DELIMITER}{key}"] = value
88
+ return tensors, metadata
89
+
90
+
91
+ def unflatten(
92
+ tensors: dict[str, torch.Tensor], metadata: dict[str, str]
93
+ ) -> dict[str, EncodedTensor]:
94
+ """
95
+ Inverse of `flatten()`, splits keys on the last delimiter.
96
+
97
+ returns:
98
+ entry name -> encoded tensor, sorted by name
99
+ """
100
+ format_version = metadata.get(FORMAT_VERSION_KEY)
101
+ if format_version != FORMAT_VERSION:
102
+ raise ValueError(
103
+ f"Not a valid VaeZip file, expected {FORMAT_VERSION_KEY}="
104
+ f"{FORMAT_VERSION!r} in the metadata, got {format_version!r}"
105
+ )
106
+ grouped_tensors: dict[str, dict[str, torch.Tensor]] = {}
107
+ for flat_key, tensor in tensors.items():
108
+ name, inner_name = _split_key(flat_key)
109
+ grouped_tensors.setdefault(name, {})[inner_name] = tensor
110
+ grouped_metadata: dict[str, dict[str, str]] = {}
111
+ for flat_key, value in metadata.items():
112
+ if flat_key in (FORMAT_VERSION_KEY, PACKAGE_VERSION_KEY):
113
+ continue
114
+ name, key = _split_key(flat_key)
115
+ if name not in grouped_tensors:
116
+ raise ValueError(
117
+ f"Not a valid VaeZip file, metadata {flat_key!r} has no tensors"
118
+ )
119
+ grouped_metadata.setdefault(name, {})[key] = value
120
+ return {
121
+ name: EncodedTensor(
122
+ tensors=grouped_tensors[name], metadata=grouped_metadata.get(name, {})
123
+ )
124
+ for name in sorted(grouped_tensors)
125
+ }
126
+
127
+
128
+ def serialize(
129
+ encoded_tensors: dict[str, EncodedTensor], format: str = SAFETENSORS_FORMAT
130
+ ) -> bytes:
131
+ check_format(format)
132
+ tensors, metadata = flatten(encoded_tensors)
133
+ if format == SAFETENSORS_FORMAT:
134
+ return save_safetensors(tensors, metadata=metadata)
135
+ buffer = io.BytesIO()
136
+ torch.save({_PTH_TENSORS_KEY: tensors, _PTH_METADATA_KEY: metadata}, buffer)
137
+ return buffer.getvalue()
138
+
139
+
140
+ def deserialize(
141
+ data: bytes, format: str = SAFETENSORS_FORMAT
142
+ ) -> dict[str, EncodedTensor]:
143
+ """returns: entry name -> encoded tensor with tensors on CPU, sorted by name"""
144
+ check_format(format)
145
+ try:
146
+ if format == SAFETENSORS_FORMAT:
147
+ tensors = load_safetensors(data)
148
+ metadata = _read_safetensors_metadata(data)
149
+ else:
150
+ content = torch.load(
151
+ io.BytesIO(data), map_location="cpu", weights_only=True
152
+ )
153
+ tensors = dict(content[_PTH_TENSORS_KEY])
154
+ metadata = dict(content[_PTH_METADATA_KEY])
155
+ except (
156
+ SafetensorError,
157
+ struct.error,
158
+ ValueError,
159
+ KeyError,
160
+ TypeError,
161
+ EOFError,
162
+ RuntimeError,
163
+ pickle.UnpicklingError,
164
+ ) as error:
165
+ raise ValueError(f"Not a valid VaeZip {format} file: {error}") from error
166
+ return unflatten(tensors, metadata)
167
+
168
+
169
+ def _check_inner_key(name: str, key: Any) -> None:
170
+ if not isinstance(key, str) or not key or KEY_DELIMITER in key:
171
+ raise ValueError(
172
+ f"Tensor and metadata names inside {name!r} must be non-empty strings "
173
+ f"without {KEY_DELIMITER!r}, got {key!r}"
174
+ )
175
+
176
+
177
+ def _split_key(flat_key: str) -> tuple[str, str]:
178
+ name, delimiter, inner_name = flat_key.rpartition(KEY_DELIMITER)
179
+ if not delimiter or not name or not inner_name:
180
+ raise ValueError(f"Not a valid VaeZip file, unexpected key {flat_key!r}")
181
+ return name, inner_name
182
+
183
+
184
+ def _read_safetensors_metadata(data: bytes) -> dict[str, str]:
185
+ (header_size,) = struct.unpack_from("<Q", data, 0)
186
+ header_end = _SAFETENSORS_HEADER_SIZE_BYTES + header_size
187
+ header = json.loads(bytes(data[_SAFETENSORS_HEADER_SIZE_BYTES:header_end]))
188
+ metadata: dict[str, str] = header.get(_SAFETENSORS_METADATA_KEY, {})
189
+ return metadata
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vaezip
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Compression of VAE latents: low-bit quantization codecs on PyTorch and safetensors
5
5
  License-Expression: Unlicense
6
6
  Project-URL: Homepage, https://github.com/tensorforger/VaeZip
@@ -62,12 +62,30 @@ import vaezip
62
62
 
63
63
  latents = torch.randn([4, 32, 128, 128], device="cuda")
64
64
 
65
- vaezip.save(latents, "latents.vz.safetensors", codec="lbrq_4bit")
66
- restored = vaezip.load("latents.vz.safetensors", device="cuda")
65
+ vaezip.save_tensor(latents, "latents.vz.safetensors", codec="lbrq_4bit")
66
+ restored = vaezip.load_tensor("latents.vz.safetensors", device="cuda")
67
67
  ```
68
68
 
69
- The codec config is stored in the file metadata, so `load()` needs no codec.
70
- Pass `save_metadata=False` to drop it, then `load()` requires an explicit `codec=`.
69
+ The codec config is stored in the file, so `load_tensor()` needs no codec.
70
+
71
+ Several tensors, each with its own codec, go to one file as a dict of `EncodedTensor`:
72
+
73
+ ```python
74
+ from vaezip.codecs import AutoCodec
75
+
76
+ tensor_a = torch.randn([4, 32, 128, 128])
77
+ tensor_b = torch.randn([4, 64, 256])
78
+ codec_b = AutoCodec.from_config("brq_8bit", block_size=(4,))
79
+
80
+ batch_dict = {
81
+ "tensor_a": vaezip.encode(tensor_a, codec="lbrq_3bit"),
82
+ "tensor_b": vaezip.encode(tensor_b, codec=codec_b),
83
+ }
84
+ vaezip.save(batch_dict, "batch_dict.vz.safetensors")
85
+
86
+ batch_dict = vaezip.load("batch_dict.vz.safetensors")
87
+ tensor_a = batch_dict["tensor_a"].decode(device="cuda")
88
+ ```
71
89
 
72
90
  ## Key concepts
73
91
 
@@ -86,13 +104,12 @@ restored = codec.decode(encoded)
86
104
  ```
87
105
 
88
106
  **EncodedTensor** holds named tensors plus string metadata with the codec config.
89
- It serializes to `.safetensors` (default) or `.pth`:
107
+ `encoded.decode()` restores the tensor with the codec from the metadata, or with an explicit `codec=`.
108
+ `vaezip.encode()` and `vaezip.decode()` are the same with codec shortcuts and configs.
90
109
 
91
- ```python
92
- data = encoded.serialize() # or vaezip.serialize(encoded)
93
- encoded = vaezip.deserialize(data)
94
- data = encoded.serialize(save_metadata=False, format="pth")
95
- ```
110
+ **Files** hold a dict of `EncodedTensor`, `vaezip.save()` and `vaezip.load()` work with paths or bytes.
111
+ The format is `.safetensors` by default, `.pth` only with `format="pth"`.
112
+ `vaezip.save_tensor()` and `vaezip.load_tensor()` are shortcuts for a file with a single tensor stored as `"tensor"`.
96
113
 
97
114
  **Configs** are diffusers-style JSON files and can be created from files, shortcut names or dicts.
98
115
  Keyword arguments override config values: