vaezip 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vaezip/__init__.py +16 -0
- vaezip/_version.py +8 -0
- vaezip/api.py +139 -0
- vaezip/codecs/__init__.py +20 -0
- vaezip/codecs/auto.py +44 -0
- vaezip/codecs/base.py +119 -0
- vaezip/codecs/block_range_quant.py +232 -0
- vaezip/codecs/cast.py +62 -0
- vaezip/codecs/config.py +55 -0
- vaezip/codecs/lloyd_block_range_quant.py +112 -0
- vaezip/codecs/range_quant.py +88 -0
- vaezip/codecs/shortcuts.py +49 -0
- vaezip/encoded_tensor.py +132 -0
- vaezip/py.typed +0 -0
- vaezip/utils/__init__.py +1 -0
- vaezip/utils/bit_packing.py +58 -0
- vaezip/utils/blocks.py +91 -0
- vaezip/utils/layout.py +50 -0
- vaezip/utils/lloyd.py +97 -0
- vaezip-0.1.0.dist-info/METADATA +160 -0
- vaezip-0.1.0.dist-info/RECORD +24 -0
- vaezip-0.1.0.dist-info/WHEEL +5 -0
- vaezip-0.1.0.dist-info/licenses/LICENSE +24 -0
- vaezip-0.1.0.dist-info/top_level.txt +1 -0
vaezip/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""VaeZip: compression of VAE latents."""
|
|
2
|
+
|
|
3
|
+
from vaezip import codecs
|
|
4
|
+
from vaezip._version import __version__
|
|
5
|
+
from vaezip.api import load, save
|
|
6
|
+
from vaezip.encoded_tensor import EncodedTensor, deserialize, serialize
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"EncodedTensor",
|
|
10
|
+
"__version__",
|
|
11
|
+
"codecs",
|
|
12
|
+
"deserialize",
|
|
13
|
+
"load",
|
|
14
|
+
"save",
|
|
15
|
+
"serialize",
|
|
16
|
+
]
|
vaezip/_version.py
ADDED
vaezip/api.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""High level save and load 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.config import without_version
|
|
14
|
+
from vaezip.codecs.shortcuts import DEFAULT_SHORTCUT
|
|
15
|
+
from vaezip.encoded_tensor import (
|
|
16
|
+
PTH_FORMAT,
|
|
17
|
+
SAFETENSORS_FORMAT,
|
|
18
|
+
EncodedTensor,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
CodecSpecification = Codec | str | os.PathLike[str]
|
|
22
|
+
PTH_SUFFIXES = (".pth", ".pt")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_codec(codec: CodecSpecification | None) -> Codec:
|
|
26
|
+
if codec is None:
|
|
27
|
+
return AutoCodec.from_config(DEFAULT_SHORTCUT)
|
|
28
|
+
if isinstance(codec, Codec):
|
|
29
|
+
return codec
|
|
30
|
+
return AutoCodec.from_config(codec)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _infer_format(path: str | os.PathLike[str], format: str | None) -> str:
|
|
34
|
+
if format is not None:
|
|
35
|
+
return format
|
|
36
|
+
return PTH_FORMAT if Path(path).suffix in PTH_SUFFIXES else SAFETENSORS_FORMAT
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@overload
|
|
40
|
+
def save(
|
|
41
|
+
tensor: torch.Tensor,
|
|
42
|
+
path: None = None,
|
|
43
|
+
codec: CodecSpecification | None = None,
|
|
44
|
+
save_metadata: bool = True,
|
|
45
|
+
format: str | None = None,
|
|
46
|
+
) -> bytes: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def save(
|
|
51
|
+
tensor: torch.Tensor,
|
|
52
|
+
path: str | os.PathLike[str],
|
|
53
|
+
codec: CodecSpecification | None = None,
|
|
54
|
+
save_metadata: bool = True,
|
|
55
|
+
format: str | None = None,
|
|
56
|
+
) -> None: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save(
|
|
60
|
+
tensor: torch.Tensor,
|
|
61
|
+
path: str | os.PathLike[str] | None = None,
|
|
62
|
+
codec: CodecSpecification | None = None,
|
|
63
|
+
save_metadata: bool = True,
|
|
64
|
+
format: str | None = None,
|
|
65
|
+
) -> bytes | None:
|
|
66
|
+
"""
|
|
67
|
+
Encodes the tensor and writes it to a file, or returns bytes if there is no path.
|
|
68
|
+
|
|
69
|
+
args:
|
|
70
|
+
tensor: [B, ...], floating point
|
|
71
|
+
path: output file, `None` to return bytes
|
|
72
|
+
codec: codec object, shortcut name or config path, default is "cast_fp32"
|
|
73
|
+
save_metadata: store the codec config, required by `load()` without a codec
|
|
74
|
+
format: "safetensors" or "pth", by default inferred from the path suffix
|
|
75
|
+
(safetensors if unknown)
|
|
76
|
+
"""
|
|
77
|
+
resolved_codec = _resolve_codec(codec)
|
|
78
|
+
if path is None:
|
|
79
|
+
serialization_format = format or SAFETENSORS_FORMAT
|
|
80
|
+
else:
|
|
81
|
+
serialization_format = _infer_format(path, format)
|
|
82
|
+
data = resolved_codec.encode(tensor).serialize(
|
|
83
|
+
save_metadata=save_metadata, format=serialization_format
|
|
84
|
+
)
|
|
85
|
+
if path is None:
|
|
86
|
+
return data
|
|
87
|
+
Path(path).write_bytes(data)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load(
|
|
92
|
+
source: str | os.PathLike[str] | bytes | bytearray,
|
|
93
|
+
codec: CodecSpecification | None = None,
|
|
94
|
+
dtype: torch.dtype = torch.float32,
|
|
95
|
+
device: torch.device | str | None = None,
|
|
96
|
+
format: str | None = None,
|
|
97
|
+
) -> torch.Tensor:
|
|
98
|
+
"""
|
|
99
|
+
Reads a file or bytes written by `save()` and decodes the tensor.
|
|
100
|
+
|
|
101
|
+
args:
|
|
102
|
+
source: file path or bytes
|
|
103
|
+
codec: codec object, shortcut name or config path, by default the codec
|
|
104
|
+
from the metadata. If both are available they must have the same config.
|
|
105
|
+
dtype: floating point dtype of the result
|
|
106
|
+
device: device to decode on, default is CPU
|
|
107
|
+
format: "safetensors" or "pth", by default inferred from the path suffix
|
|
108
|
+
(safetensors for bytes)
|
|
109
|
+
returns:
|
|
110
|
+
tensor: [B, ...], original shape, dtype: dtype
|
|
111
|
+
"""
|
|
112
|
+
if isinstance(source, (bytes, bytearray)):
|
|
113
|
+
data = bytes(source)
|
|
114
|
+
serialization_format = format or SAFETENSORS_FORMAT
|
|
115
|
+
else:
|
|
116
|
+
data = Path(source).read_bytes()
|
|
117
|
+
serialization_format = _infer_format(source, format)
|
|
118
|
+
encoded = EncodedTensor.deserialize(data, format=serialization_format)
|
|
119
|
+
stored_config = encoded.codec_config
|
|
120
|
+
|
|
121
|
+
if codec is None:
|
|
122
|
+
if stored_config is None:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"The data has no codec config in its metadata, "
|
|
125
|
+
"pass the codec explicitly"
|
|
126
|
+
)
|
|
127
|
+
resolved_codec = AutoCodec.from_config(stored_config)
|
|
128
|
+
else:
|
|
129
|
+
resolved_codec = _resolve_codec(codec)
|
|
130
|
+
if stored_config is not None and without_version(
|
|
131
|
+
stored_config
|
|
132
|
+
) != without_version(resolved_codec.get_config()):
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"The codec {resolved_codec!r} does not match the config stored in the "
|
|
135
|
+
f"data: {stored_config}"
|
|
136
|
+
)
|
|
137
|
+
if device is not None:
|
|
138
|
+
encoded = encoded.to(device)
|
|
139
|
+
return resolved_codec.decode(encoded, dtype=dtype)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Codecs for compression of VAE latents."""
|
|
2
|
+
|
|
3
|
+
from vaezip.codecs.auto import AutoCodec
|
|
4
|
+
from vaezip.codecs.base import Codec
|
|
5
|
+
from vaezip.codecs.block_range_quant import BlockRangeQuantCodec
|
|
6
|
+
from vaezip.codecs.cast import CastCodec
|
|
7
|
+
from vaezip.codecs.lloyd_block_range_quant import LloydBlockRangeQuantCodec
|
|
8
|
+
from vaezip.codecs.range_quant import RangeQuantCodec
|
|
9
|
+
from vaezip.codecs.shortcuts import DEFAULT_SHORTCUT, list_shortcuts
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DEFAULT_SHORTCUT",
|
|
13
|
+
"AutoCodec",
|
|
14
|
+
"BlockRangeQuantCodec",
|
|
15
|
+
"CastCodec",
|
|
16
|
+
"Codec",
|
|
17
|
+
"LloydBlockRangeQuantCodec",
|
|
18
|
+
"RangeQuantCodec",
|
|
19
|
+
"list_shortcuts",
|
|
20
|
+
]
|
vaezip/codecs/auto.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Construction of codecs from configs without knowing the codec class."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from vaezip.codecs.base import Codec
|
|
6
|
+
from vaezip.codecs.block_range_quant import BlockRangeQuantCodec
|
|
7
|
+
from vaezip.codecs.cast import CastCodec
|
|
8
|
+
from vaezip.codecs.config import ConfigSource, split_config
|
|
9
|
+
from vaezip.codecs.lloyd_block_range_quant import LloydBlockRangeQuantCodec
|
|
10
|
+
from vaezip.codecs.range_quant import RangeQuantCodec
|
|
11
|
+
|
|
12
|
+
CODEC_CLASSES: dict[str, type[Codec]] = {
|
|
13
|
+
codec_class.__name__: codec_class
|
|
14
|
+
for codec_class in (
|
|
15
|
+
CastCodec,
|
|
16
|
+
RangeQuantCodec,
|
|
17
|
+
BlockRangeQuantCodec,
|
|
18
|
+
LloydBlockRangeQuantCodec,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AutoCodec:
|
|
24
|
+
"""Builds the right codec class from a config."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
raise TypeError("AutoCodec is not instantiable, use AutoCodec.from_config()")
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def from_config(config: ConfigSource, **overrides: Any) -> Codec:
|
|
31
|
+
"""
|
|
32
|
+
args:
|
|
33
|
+
config: path to a JSON config, a shortcut name such as "cast_fp16",
|
|
34
|
+
or a config dict
|
|
35
|
+
overrides: constructor parameters that replace the ones from the config
|
|
36
|
+
"""
|
|
37
|
+
class_name, parameters = split_config(config, overrides)
|
|
38
|
+
if class_name is None:
|
|
39
|
+
raise ValueError("Config has no '_class_name' entry")
|
|
40
|
+
if class_name not in CODEC_CLASSES:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Unknown codec class {class_name!r}, available: {list(CODEC_CLASSES)}"
|
|
43
|
+
)
|
|
44
|
+
return CODEC_CLASSES[class_name](**parameters)
|
vaezip/codecs/base.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Abstract codec API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, TypeVar
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from vaezip._version import __version__
|
|
13
|
+
from vaezip.codecs.config import ConfigSource, split_config
|
|
14
|
+
from vaezip.codecs.shortcuts import CODEC_CLASS_KEY, VERSION_KEY
|
|
15
|
+
from vaezip.encoded_tensor import CODEC_CONFIG_KEY, EncodedTensor
|
|
16
|
+
|
|
17
|
+
CodecType = TypeVar("CodecType", bound="Codec")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Codec(ABC):
|
|
21
|
+
"""
|
|
22
|
+
Compression algorithm for tensors.
|
|
23
|
+
|
|
24
|
+
The configuration is fixed at construction. Encoding takes a tensor and returns an
|
|
25
|
+
`EncodedTensor` on the same device, decoding restores a float tensor of the
|
|
26
|
+
original shape.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def encode(self, tensor: torch.Tensor) -> EncodedTensor:
|
|
30
|
+
"""
|
|
31
|
+
args:
|
|
32
|
+
tensor: [B, ...], floating point, finite values
|
|
33
|
+
returns:
|
|
34
|
+
encoded: tensors on the device of the input, metadata with the codec config
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(tensor, torch.Tensor):
|
|
37
|
+
raise TypeError(f"Expected a torch.Tensor, got {type(tensor).__name__}")
|
|
38
|
+
if not tensor.is_floating_point():
|
|
39
|
+
raise TypeError(f"Expected a floating point tensor, got {tensor.dtype}")
|
|
40
|
+
if tensor.numel() == 0:
|
|
41
|
+
raise ValueError("Can not encode an empty tensor")
|
|
42
|
+
tensors = self._encode_tensors(tensor.detach())
|
|
43
|
+
metadata = {CODEC_CONFIG_KEY: json.dumps(self.get_config())}
|
|
44
|
+
return EncodedTensor(tensors=tensors, metadata=metadata)
|
|
45
|
+
|
|
46
|
+
def decode(
|
|
47
|
+
self, encoded: EncodedTensor, dtype: torch.dtype = torch.float32
|
|
48
|
+
) -> torch.Tensor:
|
|
49
|
+
"""
|
|
50
|
+
args:
|
|
51
|
+
encoded: output of `encode()` of the same codec configuration
|
|
52
|
+
dtype: floating point dtype of the result
|
|
53
|
+
returns:
|
|
54
|
+
tensor: [B, ...], original shape, dtype: dtype, device of encoded tensors
|
|
55
|
+
"""
|
|
56
|
+
if not dtype.is_floating_point:
|
|
57
|
+
raise TypeError(f"dtype must be a floating point dtype, got {dtype}")
|
|
58
|
+
return self._decode_tensors(encoded.tensors, dtype)
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def _encode_tensors(self, tensor: torch.Tensor) -> dict[str, torch.Tensor]:
|
|
62
|
+
"""
|
|
63
|
+
args:
|
|
64
|
+
tensor: [B, ...], floating point
|
|
65
|
+
returns:
|
|
66
|
+
tensors: compressed representation, per-sample tensors have the batch first
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def _decode_tensors(
|
|
71
|
+
self, tensors: dict[str, torch.Tensor], dtype: torch.dtype
|
|
72
|
+
) -> torch.Tensor:
|
|
73
|
+
"""
|
|
74
|
+
args:
|
|
75
|
+
tensors: output of `_encode_tensors()`
|
|
76
|
+
dtype: floating point dtype of the result
|
|
77
|
+
returns:
|
|
78
|
+
tensor: [B, ...], original shape, dtype: dtype
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def get_config_parameters(self) -> dict[str, Any]:
|
|
83
|
+
"""JSON-serializable constructor parameters, `from_config` accepts them back."""
|
|
84
|
+
|
|
85
|
+
def get_config(self) -> dict[str, Any]:
|
|
86
|
+
"""Diffusers-style config: class name, package version and the parameters."""
|
|
87
|
+
return {
|
|
88
|
+
CODEC_CLASS_KEY: type(self).__name__,
|
|
89
|
+
VERSION_KEY: __version__,
|
|
90
|
+
**self.get_config_parameters(),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def save_config(self, path: str | os.PathLike[str]) -> None:
|
|
94
|
+
with open(path, "w", encoding="utf-8") as config_file:
|
|
95
|
+
json.dump(self.get_config(), config_file, indent=2)
|
|
96
|
+
config_file.write("\n")
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_config(
|
|
100
|
+
cls: type[CodecType], config: ConfigSource, **overrides: Any
|
|
101
|
+
) -> CodecType:
|
|
102
|
+
"""
|
|
103
|
+
args:
|
|
104
|
+
config: path to a JSON config, a shortcut name or a config dict
|
|
105
|
+
overrides: constructor parameters that replace the ones from the config
|
|
106
|
+
"""
|
|
107
|
+
class_name, parameters = split_config(config, overrides)
|
|
108
|
+
if class_name is not None and class_name != cls.__name__:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Config describes {class_name}, "
|
|
111
|
+
f"can not construct {cls.__name__} from it"
|
|
112
|
+
)
|
|
113
|
+
return cls(**parameters)
|
|
114
|
+
|
|
115
|
+
def __repr__(self) -> str:
|
|
116
|
+
parameters = ", ".join(
|
|
117
|
+
f"{name}={value!r}" for name, value in self.get_config_parameters().items()
|
|
118
|
+
)
|
|
119
|
+
return f"{type(self).__name__}({parameters})"
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Blockwise range quantization to low-bit integers."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from vaezip.codecs.base import Codec
|
|
10
|
+
from vaezip.codecs.range_quant import (
|
|
11
|
+
CHANNEL_MAX_KEY,
|
|
12
|
+
CHANNEL_MIN_KEY,
|
|
13
|
+
CODES_KEY,
|
|
14
|
+
MIN_RANGE,
|
|
15
|
+
SHAPE_KEY,
|
|
16
|
+
)
|
|
17
|
+
from vaezip.utils.bit_packing import check_bits, max_code, pack_bits, unpack_bits
|
|
18
|
+
from vaezip.utils.blocks import merge_blocks, padded_size, split_into_blocks
|
|
19
|
+
from vaezip.utils.layout import (
|
|
20
|
+
channel_from_second,
|
|
21
|
+
channel_to_second,
|
|
22
|
+
check_channel_dim,
|
|
23
|
+
shape_with_channel_second,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
RANGE_LOWER_KEY = "range_lower"
|
|
27
|
+
RANGE_UPPER_KEY = "range_upper"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_block_size(block_size: Sequence[int]) -> tuple[int, ...]:
|
|
31
|
+
if isinstance(block_size, (str, bytes)) or not isinstance(block_size, Sequence):
|
|
32
|
+
raise TypeError("block_size must be a sequence of ints")
|
|
33
|
+
parsed = tuple(block_size)
|
|
34
|
+
if not parsed:
|
|
35
|
+
raise ValueError("block_size must have at least one dimension")
|
|
36
|
+
for extent in parsed:
|
|
37
|
+
if isinstance(extent, bool) or not isinstance(extent, int) or extent < 1:
|
|
38
|
+
raise ValueError(f"block_size must contain positive ints, got {block_size}")
|
|
39
|
+
return parsed
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BlockRangeQuantCodec(Codec):
|
|
43
|
+
"""
|
|
44
|
+
Quantizes each block of each channel between its own min and max.
|
|
45
|
+
The block ranges are quantized to `range_bits` relative to the min and max of the
|
|
46
|
+
whole channel, the values to `value_bits` inside the block range. Everything is
|
|
47
|
+
packed into bytes.
|
|
48
|
+
|
|
49
|
+
The first dimension is the batch, one other dimension is the channel, the remaining
|
|
50
|
+
dimensions are spatial and `block_size` has one entry for each of them. Spatial
|
|
51
|
+
sizes that are not divisible by the block size are padded internally, the decoded
|
|
52
|
+
tensor has the original shape.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
value_bits: int = 4,
|
|
58
|
+
range_bits: int = 4,
|
|
59
|
+
channel_dim: int = 1,
|
|
60
|
+
block_size: Sequence[int] = (4, 4),
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__()
|
|
63
|
+
check_bits(value_bits, "value_bits")
|
|
64
|
+
check_bits(range_bits, "range_bits")
|
|
65
|
+
check_channel_dim(channel_dim)
|
|
66
|
+
self.value_bits = value_bits
|
|
67
|
+
self.range_bits = range_bits
|
|
68
|
+
self.channel_dim = channel_dim
|
|
69
|
+
self.block_size = parse_block_size(block_size)
|
|
70
|
+
|
|
71
|
+
def _encode_tensors(self, tensor: torch.Tensor) -> dict[str, torch.Tensor]:
|
|
72
|
+
"""
|
|
73
|
+
args:
|
|
74
|
+
tensor: [B, ...] with ndim == len(block_size) + 2, floating point
|
|
75
|
+
returns:
|
|
76
|
+
tensors: codes: [B, ceil(C * G * V * value_bits / 8)],
|
|
77
|
+
range_lower, range_upper: [B, ceil(C * G * range_bits / 8)],
|
|
78
|
+
channel_min, channel_max: [B, C], shape: [ndim], dtype: torch.int64,
|
|
79
|
+
G: number of blocks per channel, V: number of values per block
|
|
80
|
+
"""
|
|
81
|
+
if tensor.ndim != len(self.block_size) + 2:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"Expected a tensor with {len(self.block_size) + 2} dimensions "
|
|
84
|
+
f"(batch, channel and {len(self.block_size)} spatial), got shape "
|
|
85
|
+
f"{tuple(tensor.shape)}"
|
|
86
|
+
)
|
|
87
|
+
channel_first = channel_to_second(tensor.float(), self.channel_dim)
|
|
88
|
+
blocks = split_into_blocks(channel_first, self.block_size) # [B, C, G, V]
|
|
89
|
+
batch_size = blocks.shape[0]
|
|
90
|
+
range_levels = max_code(self.range_bits)
|
|
91
|
+
|
|
92
|
+
channel_min = blocks.amin(dim=(2, 3), keepdim=True) # [B, C, 1, 1]
|
|
93
|
+
channel_max = blocks.amax(dim=(2, 3), keepdim=True)
|
|
94
|
+
range_step = _range_step(channel_min, channel_max, range_levels)
|
|
95
|
+
block_min = blocks.amin(dim=3, keepdim=True) # [B, C, G, 1]
|
|
96
|
+
block_max = blocks.amax(dim=3, keepdim=True)
|
|
97
|
+
lower = torch.floor((block_min - channel_min) / range_step)
|
|
98
|
+
lower = lower.clamp(0, range_levels - 1)
|
|
99
|
+
upper = torch.ceil((block_max - channel_min) / range_step).clamp(
|
|
100
|
+
max=range_levels
|
|
101
|
+
)
|
|
102
|
+
upper = torch.maximum(upper, lower + 1)
|
|
103
|
+
start, span = _block_grid(lower, upper, channel_min, range_step)
|
|
104
|
+
|
|
105
|
+
normalized = ((blocks - start) / span).clamp(0, 1)
|
|
106
|
+
codes, quantizer_tensors = self._quantize_normalized(normalized, span)
|
|
107
|
+
return {
|
|
108
|
+
CODES_KEY: pack_bits(codes.reshape(batch_size, -1), self.value_bits),
|
|
109
|
+
RANGE_LOWER_KEY: pack_bits(
|
|
110
|
+
lower.reshape(batch_size, -1).to(torch.uint8), self.range_bits
|
|
111
|
+
),
|
|
112
|
+
RANGE_UPPER_KEY: pack_bits(
|
|
113
|
+
upper.reshape(batch_size, -1).to(torch.uint8), self.range_bits
|
|
114
|
+
),
|
|
115
|
+
CHANNEL_MIN_KEY: channel_min.reshape(batch_size, -1),
|
|
116
|
+
CHANNEL_MAX_KEY: channel_max.reshape(batch_size, -1),
|
|
117
|
+
SHAPE_KEY: torch.tensor(
|
|
118
|
+
tensor.shape, dtype=torch.int64, device=tensor.device
|
|
119
|
+
),
|
|
120
|
+
**quantizer_tensors,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def _decode_tensors(
|
|
124
|
+
self, tensors: dict[str, torch.Tensor], dtype: torch.dtype
|
|
125
|
+
) -> torch.Tensor:
|
|
126
|
+
"""
|
|
127
|
+
args:
|
|
128
|
+
tensors: output of `_encode_tensors()`
|
|
129
|
+
returns:
|
|
130
|
+
tensor: [B, ...], dtype: dtype
|
|
131
|
+
"""
|
|
132
|
+
shape = [int(size) for size in tensors[SHAPE_KEY].tolist()]
|
|
133
|
+
if len(shape) != len(self.block_size) + 2:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Encoded tensor has {len(shape)} dimensions, "
|
|
136
|
+
f"this codec expects {len(self.block_size) + 2}"
|
|
137
|
+
)
|
|
138
|
+
batch_size, channels, *spatial_shape = shape_with_channel_second(
|
|
139
|
+
shape, self.channel_dim
|
|
140
|
+
)
|
|
141
|
+
blocks_per_channel = math.prod(
|
|
142
|
+
padded_size(size, extent) // extent
|
|
143
|
+
for size, extent in zip(spatial_shape, self.block_size, strict=True)
|
|
144
|
+
)
|
|
145
|
+
values_per_block = math.prod(self.block_size)
|
|
146
|
+
range_levels = max_code(self.range_bits)
|
|
147
|
+
|
|
148
|
+
ranges_count = channels * blocks_per_channel
|
|
149
|
+
lower = unpack_bits(tensors[RANGE_LOWER_KEY], ranges_count, self.range_bits)
|
|
150
|
+
upper = unpack_bits(tensors[RANGE_UPPER_KEY], ranges_count, self.range_bits)
|
|
151
|
+
lower = lower.reshape(batch_size, channels, blocks_per_channel, 1).float()
|
|
152
|
+
upper = upper.reshape(batch_size, channels, blocks_per_channel, 1).float()
|
|
153
|
+
channel_min = tensors[CHANNEL_MIN_KEY].reshape(batch_size, channels, 1, 1)
|
|
154
|
+
channel_max = tensors[CHANNEL_MAX_KEY].reshape(batch_size, channels, 1, 1)
|
|
155
|
+
range_step = _range_step(channel_min, channel_max, range_levels)
|
|
156
|
+
start, span = _block_grid(lower, upper, channel_min, range_step)
|
|
157
|
+
|
|
158
|
+
codes = unpack_bits(
|
|
159
|
+
tensors[CODES_KEY], ranges_count * values_per_block, self.value_bits
|
|
160
|
+
)
|
|
161
|
+
codes = codes.reshape(
|
|
162
|
+
batch_size, channels, blocks_per_channel, values_per_block
|
|
163
|
+
)
|
|
164
|
+
normalized = self._dequantize_normalized(codes, tensors)
|
|
165
|
+
blocks = normalized * span + start # [B, C, G, V]
|
|
166
|
+
channel_first = merge_blocks(blocks, spatial_shape, self.block_size)
|
|
167
|
+
return (
|
|
168
|
+
channel_from_second(channel_first, self.channel_dim).to(dtype).contiguous()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def _quantize_normalized(
|
|
172
|
+
self, normalized: torch.Tensor, span: torch.Tensor
|
|
173
|
+
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
|
|
174
|
+
"""
|
|
175
|
+
args:
|
|
176
|
+
normalized: [B, C, G, V], values: [0...1], position inside the block range
|
|
177
|
+
span: [B, C, G, 1], width of the block range
|
|
178
|
+
returns:
|
|
179
|
+
codes: [B, C, G, V], dtype: torch.uint8, values: [0...2^value_bits - 1]
|
|
180
|
+
tensors: additional tensors needed for dequantization
|
|
181
|
+
"""
|
|
182
|
+
codes = torch.round(normalized * max_code(self.value_bits))
|
|
183
|
+
return codes.to(torch.uint8), {}
|
|
184
|
+
|
|
185
|
+
def _dequantize_normalized(
|
|
186
|
+
self, codes: torch.Tensor, tensors: dict[str, torch.Tensor]
|
|
187
|
+
) -> torch.Tensor:
|
|
188
|
+
"""
|
|
189
|
+
args:
|
|
190
|
+
codes: [B, C, G, V], dtype: torch.uint8
|
|
191
|
+
tensors: output of `_encode_tensors()`
|
|
192
|
+
returns:
|
|
193
|
+
normalized: [B, C, G, V], dtype: torch.float32, values: [0...1]
|
|
194
|
+
"""
|
|
195
|
+
return codes.float() / max_code(self.value_bits)
|
|
196
|
+
|
|
197
|
+
def get_config_parameters(self) -> dict[str, Any]:
|
|
198
|
+
return {
|
|
199
|
+
"value_bits": self.value_bits,
|
|
200
|
+
"range_bits": self.range_bits,
|
|
201
|
+
"channel_dim": self.channel_dim,
|
|
202
|
+
"block_size": list(self.block_size),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _range_step(
|
|
207
|
+
channel_min: torch.Tensor, channel_max: torch.Tensor, range_levels: int
|
|
208
|
+
) -> torch.Tensor:
|
|
209
|
+
"""
|
|
210
|
+
args:
|
|
211
|
+
channel_min, channel_max: [B, C, 1, 1]
|
|
212
|
+
returns:
|
|
213
|
+
range_step: [B, C, 1, 1], distance between neighbouring block range positions
|
|
214
|
+
"""
|
|
215
|
+
return ((channel_max - channel_min) / range_levels).clamp(min=MIN_RANGE)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _block_grid(
|
|
219
|
+
lower: torch.Tensor,
|
|
220
|
+
upper: torch.Tensor,
|
|
221
|
+
channel_min: torch.Tensor,
|
|
222
|
+
range_step: torch.Tensor,
|
|
223
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
224
|
+
"""
|
|
225
|
+
args:
|
|
226
|
+
lower, upper: [B, C, G, 1], dtype: torch.float32, block range positions
|
|
227
|
+
channel_min, range_step: [B, C, 1, 1]
|
|
228
|
+
returns:
|
|
229
|
+
start: [B, C, G, 1], lower end of the block range
|
|
230
|
+
span: [B, C, G, 1], width of the block range
|
|
231
|
+
"""
|
|
232
|
+
return channel_min + lower * range_step, (upper - lower) * range_step
|
vaezip/codecs/cast.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Codec that casts values to a lower precision floating point dtype."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
from vaezip.codecs.base import Codec
|
|
8
|
+
|
|
9
|
+
VALUES_KEY = "values"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_dtype(dtype: torch.dtype | str) -> torch.dtype:
|
|
13
|
+
"""Accepts a torch dtype or its name such as "float16"."""
|
|
14
|
+
if isinstance(dtype, str):
|
|
15
|
+
name = dtype.removeprefix("torch.")
|
|
16
|
+
resolved = getattr(torch, name, None)
|
|
17
|
+
if not isinstance(resolved, torch.dtype):
|
|
18
|
+
raise ValueError(f"Unknown torch dtype {dtype!r}")
|
|
19
|
+
return resolved
|
|
20
|
+
if isinstance(dtype, torch.dtype):
|
|
21
|
+
return dtype
|
|
22
|
+
raise TypeError(
|
|
23
|
+
f"Expected a torch.dtype or a dtype name, got {type(dtype).__name__}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CastCodec(Codec):
|
|
28
|
+
"""Stores the tensor in a floating point dtype, out-of-range values are clamped."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, cast_dtype: torch.dtype | str = torch.float16) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.cast_dtype = parse_dtype(cast_dtype)
|
|
33
|
+
if not self.cast_dtype.is_floating_point:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"cast_dtype must be floating point, got {self.cast_dtype}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _encode_tensors(self, tensor: torch.Tensor) -> dict[str, torch.Tensor]:
|
|
39
|
+
"""
|
|
40
|
+
args:
|
|
41
|
+
tensor: [B, ...], floating point
|
|
42
|
+
returns:
|
|
43
|
+
tensors: values: [B, ...], dtype: cast_dtype
|
|
44
|
+
"""
|
|
45
|
+
cast_max = torch.finfo(self.cast_dtype).max
|
|
46
|
+
if cast_max < torch.finfo(tensor.dtype).max:
|
|
47
|
+
tensor = tensor.clamp(-cast_max, cast_max)
|
|
48
|
+
return {VALUES_KEY: tensor.to(self.cast_dtype)}
|
|
49
|
+
|
|
50
|
+
def _decode_tensors(
|
|
51
|
+
self, tensors: dict[str, torch.Tensor], dtype: torch.dtype
|
|
52
|
+
) -> torch.Tensor:
|
|
53
|
+
"""
|
|
54
|
+
args:
|
|
55
|
+
tensors: values: [B, ...], dtype: cast_dtype
|
|
56
|
+
returns:
|
|
57
|
+
tensor: [B, ...], dtype: dtype
|
|
58
|
+
"""
|
|
59
|
+
return tensors[VALUES_KEY].to(dtype)
|
|
60
|
+
|
|
61
|
+
def get_config_parameters(self) -> dict[str, Any]:
|
|
62
|
+
return {"cast_dtype": str(self.cast_dtype).removeprefix("torch.")}
|