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 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
@@ -0,0 +1,8 @@
1
+ """Package version."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("vaezip")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0+unknown"
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.")}