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.
- {vaezip-0.1.0/src/vaezip.egg-info → vaezip-0.2.0}/PKG-INFO +28 -11
- {vaezip-0.1.0 → vaezip-0.2.0}/README.md +27 -10
- {vaezip-0.1.0 → vaezip-0.2.0}/pyproject.toml +1 -1
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/__init__.py +6 -4
- vaezip-0.2.0/src/vaezip/api.py +188 -0
- vaezip-0.2.0/src/vaezip/encoded_tensor.py +93 -0
- vaezip-0.2.0/src/vaezip/serialization.py +189 -0
- {vaezip-0.1.0 → vaezip-0.2.0/src/vaezip.egg-info}/PKG-INFO +28 -11
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip.egg-info/SOURCES.txt +2 -0
- vaezip-0.2.0/tests/test_api.py +237 -0
- vaezip-0.2.0/tests/test_encoded_tensor.py +111 -0
- vaezip-0.2.0/tests/test_serialization.py +181 -0
- vaezip-0.1.0/src/vaezip/api.py +0 -139
- vaezip-0.1.0/src/vaezip/encoded_tensor.py +0 -132
- vaezip-0.1.0/tests/test_api.py +0 -142
- vaezip-0.1.0/tests/test_encoded_tensor.py +0 -96
- {vaezip-0.1.0 → vaezip-0.2.0}/LICENSE +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/setup.cfg +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/_version.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/__init__.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/auto.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/base.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/block_range_quant.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/cast.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/config.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/lloyd_block_range_quant.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/range_quant.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/codecs/shortcuts.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/py.typed +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/__init__.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/bit_packing.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/blocks.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/layout.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip/utils/lloyd.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip.egg-info/dependency_links.txt +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip.egg-info/requires.txt +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/src/vaezip.egg-info/top_level.txt +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_block_range_quant_codec.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_cast_codec.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_config.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_lloyd_block_range_quant_codec.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_package.py +0 -0
- {vaezip-0.1.0 → vaezip-0.2.0}/tests/test_range_quant_codec.py +0 -0
- {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.
|
|
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.
|
|
66
|
-
restored = vaezip.
|
|
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
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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.
|
|
37
|
-
restored = vaezip.
|
|
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
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
@@ -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
|
|
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
|
-
"
|
|
12
|
+
"decode",
|
|
13
|
+
"encode",
|
|
13
14
|
"load",
|
|
15
|
+
"load_tensor",
|
|
14
16
|
"save",
|
|
15
|
-
"
|
|
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.
|
|
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.
|
|
66
|
-
restored = vaezip.
|
|
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
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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:
|