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/codecs/config.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Resolving codec configs from dicts, files and shortcut names."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from vaezip.codecs.shortcuts import (
|
|
9
|
+
CODEC_CLASS_KEY,
|
|
10
|
+
SHORTCUT_CONFIGS,
|
|
11
|
+
VERSION_KEY,
|
|
12
|
+
list_shortcuts,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
ConfigSource = str | os.PathLike[str] | dict[str, Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_config(source: ConfigSource) -> dict[str, Any]:
|
|
19
|
+
"""Config dict from a config dict, a shortcut name or a path to a JSON file."""
|
|
20
|
+
if isinstance(source, dict):
|
|
21
|
+
return copy.deepcopy(source)
|
|
22
|
+
if isinstance(source, str) and source in SHORTCUT_CONFIGS:
|
|
23
|
+
return copy.deepcopy(SHORTCUT_CONFIGS[source])
|
|
24
|
+
path = os.fspath(source)
|
|
25
|
+
if not os.path.isfile(path):
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"{path!r} is neither a codec config file nor a shortcut name, "
|
|
28
|
+
f"available shortcuts: {list_shortcuts()}"
|
|
29
|
+
)
|
|
30
|
+
with open(path, encoding="utf-8") as config_file:
|
|
31
|
+
config = json.load(config_file)
|
|
32
|
+
if not isinstance(config, dict):
|
|
33
|
+
raise ValueError(f"Codec config {path!r} must contain a JSON object")
|
|
34
|
+
return config
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def split_config(
|
|
38
|
+
source: ConfigSource, overrides: dict[str, Any]
|
|
39
|
+
) -> tuple[str | None, dict[str, Any]]:
|
|
40
|
+
"""
|
|
41
|
+
returns:
|
|
42
|
+
class name from the config (if present), constructor parameters with the
|
|
43
|
+
overrides applied
|
|
44
|
+
"""
|
|
45
|
+
config = load_config(source)
|
|
46
|
+
class_name = config.pop(CODEC_CLASS_KEY, None)
|
|
47
|
+
parameters = {
|
|
48
|
+
name: value for name, value in config.items() if not name.startswith("_")
|
|
49
|
+
}
|
|
50
|
+
parameters.update(overrides)
|
|
51
|
+
return class_name, parameters
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def without_version(config: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
return {name: value for name, value in config.items() if name != VERSION_KEY}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Blockwise range quantization with Lloyd-Max level tables."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
from vaezip.codecs.block_range_quant import BlockRangeQuantCodec
|
|
9
|
+
from vaezip.utils.lloyd import fit_lloyd_table, fit_lloyd_tables_fast, nearest_level
|
|
10
|
+
|
|
11
|
+
TABLE_KEY = "table"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LloydBlockRangeQuantCodec(BlockRangeQuantCodec):
|
|
15
|
+
"""
|
|
16
|
+
`BlockRangeQuantCodec` where the levels inside each block range are not uniform but
|
|
17
|
+
follow a Lloyd-Max table fitted to the values of each sample and stored in the
|
|
18
|
+
encoded tensor.
|
|
19
|
+
|
|
20
|
+
lloyd_fast: fit the tables with a fast histogram-based approximation.
|
|
21
|
+
cache_table: fit one table on the first encoded batch and reuse it for all next
|
|
22
|
+
calls. This speeds up encoding only, decoding always uses the table stored in the
|
|
23
|
+
encoded tensor.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
value_bits: int = 4,
|
|
29
|
+
range_bits: int = 4,
|
|
30
|
+
channel_dim: int = 1,
|
|
31
|
+
block_size: Sequence[int] = (4, 4),
|
|
32
|
+
lloyd_fast: bool = True,
|
|
33
|
+
cache_table: bool = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__(
|
|
36
|
+
value_bits=value_bits,
|
|
37
|
+
range_bits=range_bits,
|
|
38
|
+
channel_dim=channel_dim,
|
|
39
|
+
block_size=block_size,
|
|
40
|
+
)
|
|
41
|
+
self.lloyd_fast = lloyd_fast
|
|
42
|
+
self.cache_table = cache_table
|
|
43
|
+
self._cached_table: torch.Tensor | None = None
|
|
44
|
+
|
|
45
|
+
def flush_cache(self) -> None:
|
|
46
|
+
"""Drops the cached table, the next encoded batch fits a new one."""
|
|
47
|
+
self._cached_table = None
|
|
48
|
+
|
|
49
|
+
def _quantize_normalized(
|
|
50
|
+
self, normalized: torch.Tensor, span: torch.Tensor
|
|
51
|
+
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
|
|
52
|
+
"""
|
|
53
|
+
args:
|
|
54
|
+
normalized: [B, C, G, V], values: [0...1], position inside the block range
|
|
55
|
+
span: [B, C, G, 1], width of the block range
|
|
56
|
+
returns:
|
|
57
|
+
codes: [B, C, G, V], dtype: torch.uint8, values: [0...2^value_bits - 1]
|
|
58
|
+
tensors: table: [B, 2^value_bits], dtype: torch.float16, sorted, [0...1]
|
|
59
|
+
"""
|
|
60
|
+
batch_size = normalized.shape[0]
|
|
61
|
+
values = normalized.reshape(batch_size, -1)
|
|
62
|
+
if self._cached_table is not None:
|
|
63
|
+
tables = self._cached_table.to(values.device).unsqueeze(0)
|
|
64
|
+
else:
|
|
65
|
+
weights = (span.expand_as(normalized) ** 2).reshape(batch_size, -1)
|
|
66
|
+
if self.cache_table:
|
|
67
|
+
tables = self._fit_tables(values.reshape(1, -1), weights.reshape(1, -1))
|
|
68
|
+
self._cached_table = tables[0].clone()
|
|
69
|
+
else:
|
|
70
|
+
tables = self._fit_tables(values, weights)
|
|
71
|
+
tables = tables.expand(batch_size, -1).contiguous()
|
|
72
|
+
codes = nearest_level(values, tables).reshape(normalized.shape)
|
|
73
|
+
return codes, {TABLE_KEY: tables.to(torch.float16)}
|
|
74
|
+
|
|
75
|
+
def _dequantize_normalized(
|
|
76
|
+
self, codes: torch.Tensor, tensors: dict[str, torch.Tensor]
|
|
77
|
+
) -> torch.Tensor:
|
|
78
|
+
"""
|
|
79
|
+
args:
|
|
80
|
+
codes: [B, C, G, V], dtype: torch.uint8
|
|
81
|
+
tensors: output of `_encode_tensors()`, table: [B, 2^value_bits]
|
|
82
|
+
returns:
|
|
83
|
+
normalized: [B, C, G, V], dtype: torch.float32, values: [0...1]
|
|
84
|
+
"""
|
|
85
|
+
batch_size = codes.shape[0]
|
|
86
|
+
table = tensors[TABLE_KEY].float()
|
|
87
|
+
levels = table.gather(1, codes.reshape(batch_size, -1).long())
|
|
88
|
+
return levels.reshape(codes.shape)
|
|
89
|
+
|
|
90
|
+
def _fit_tables(self, values: torch.Tensor, weights: torch.Tensor) -> torch.Tensor:
|
|
91
|
+
"""
|
|
92
|
+
args:
|
|
93
|
+
values, weights: [R, N]
|
|
94
|
+
returns:
|
|
95
|
+
tables: [R, 2^value_bits], dtype: torch.float32
|
|
96
|
+
"""
|
|
97
|
+
levels = 2**self.value_bits
|
|
98
|
+
if self.lloyd_fast:
|
|
99
|
+
return fit_lloyd_tables_fast(values, weights, levels)
|
|
100
|
+
return torch.stack(
|
|
101
|
+
[
|
|
102
|
+
fit_lloyd_table(row_values, row_weights, levels)
|
|
103
|
+
for row_values, row_weights in zip(values, weights, strict=True)
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def get_config_parameters(self) -> dict[str, Any]:
|
|
108
|
+
return {
|
|
109
|
+
**super().get_config_parameters(),
|
|
110
|
+
"lloyd_fast": self.lloyd_fast,
|
|
111
|
+
"cache_table": self.cache_table,
|
|
112
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Per-channel range quantization to low-bit integers."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
from vaezip.codecs.base import Codec
|
|
9
|
+
from vaezip.utils.bit_packing import check_bits, max_code, pack_bits, unpack_bits
|
|
10
|
+
from vaezip.utils.layout import (
|
|
11
|
+
channel_from_second,
|
|
12
|
+
channel_to_second,
|
|
13
|
+
check_channel_dim,
|
|
14
|
+
shape_with_channel_second,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
CODES_KEY = "codes"
|
|
18
|
+
CHANNEL_MIN_KEY = "channel_min"
|
|
19
|
+
CHANNEL_MAX_KEY = "channel_max"
|
|
20
|
+
SHAPE_KEY = "shape"
|
|
21
|
+
MIN_RANGE = torch.finfo(torch.float32).tiny
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RangeQuantCodec(Codec):
|
|
25
|
+
"""
|
|
26
|
+
Normalizes each channel of each sample by its min and max, then quantizes to
|
|
27
|
+
`bits`-bit integers packed into bytes. The first dimension is the batch, any number
|
|
28
|
+
of spatial dimensions is supported.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, bits: int = 8, channel_dim: int = 1) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
check_bits(bits, "bits")
|
|
34
|
+
check_channel_dim(channel_dim)
|
|
35
|
+
self.bits = bits
|
|
36
|
+
self.channel_dim = channel_dim
|
|
37
|
+
|
|
38
|
+
def _encode_tensors(self, tensor: torch.Tensor) -> dict[str, torch.Tensor]:
|
|
39
|
+
"""
|
|
40
|
+
args:
|
|
41
|
+
tensor: [B, ...], floating point, one channel dimension at channel_dim
|
|
42
|
+
returns:
|
|
43
|
+
tensors: codes: [B, ceil(N * bits / 8)], channel_min, channel_max: [B, C],
|
|
44
|
+
shape: [ndim], dtype: torch.int64, N: number of values per sample
|
|
45
|
+
"""
|
|
46
|
+
channel_first = channel_to_second(tensor.float(), self.channel_dim)
|
|
47
|
+
batch_size, channels = channel_first.shape[:2]
|
|
48
|
+
values = channel_first.reshape(batch_size, channels, -1) # [B, C, S]
|
|
49
|
+
channel_min = values.amin(dim=2, keepdim=True)
|
|
50
|
+
channel_max = values.amax(dim=2, keepdim=True)
|
|
51
|
+
step = (channel_max - channel_min).clamp(min=MIN_RANGE) / max_code(self.bits)
|
|
52
|
+
codes = torch.round((values - channel_min) / step)
|
|
53
|
+
codes = codes.clamp(0, max_code(self.bits)).to(torch.uint8)
|
|
54
|
+
return {
|
|
55
|
+
CODES_KEY: pack_bits(codes.reshape(batch_size, -1), self.bits),
|
|
56
|
+
CHANNEL_MIN_KEY: channel_min.reshape(batch_size, channels),
|
|
57
|
+
CHANNEL_MAX_KEY: channel_max.reshape(batch_size, channels),
|
|
58
|
+
SHAPE_KEY: torch.tensor(
|
|
59
|
+
tensor.shape, dtype=torch.int64, device=tensor.device
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def _decode_tensors(
|
|
64
|
+
self, tensors: dict[str, torch.Tensor], dtype: torch.dtype
|
|
65
|
+
) -> torch.Tensor:
|
|
66
|
+
"""
|
|
67
|
+
args:
|
|
68
|
+
tensors: output of `_encode_tensors()`
|
|
69
|
+
returns:
|
|
70
|
+
tensor: [B, ...], dtype: dtype
|
|
71
|
+
"""
|
|
72
|
+
shape = [int(size) for size in tensors[SHAPE_KEY].tolist()]
|
|
73
|
+
batch_size, channels, *spatial_shape = shape_with_channel_second(
|
|
74
|
+
shape, self.channel_dim
|
|
75
|
+
)
|
|
76
|
+
count = channels * math.prod(spatial_shape)
|
|
77
|
+
codes = unpack_bits(tensors[CODES_KEY], count, self.bits)
|
|
78
|
+
codes = codes.reshape(batch_size, channels, -1).float() # [B, C, S]
|
|
79
|
+
channel_min = tensors[CHANNEL_MIN_KEY].reshape(batch_size, channels, 1)
|
|
80
|
+
channel_max = tensors[CHANNEL_MAX_KEY].reshape(batch_size, channels, 1)
|
|
81
|
+
step = (channel_max - channel_min).clamp(min=MIN_RANGE) / max_code(self.bits)
|
|
82
|
+
values = (codes * step + channel_min).reshape(
|
|
83
|
+
batch_size, channels, *spatial_shape
|
|
84
|
+
)
|
|
85
|
+
return channel_from_second(values, self.channel_dim).to(dtype).contiguous()
|
|
86
|
+
|
|
87
|
+
def get_config_parameters(self) -> dict[str, Any]:
|
|
88
|
+
return {"bits": self.bits, "channel_dim": self.channel_dim}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Predefined codec configs addressed by short names."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
CODEC_CLASS_KEY = "_class_name"
|
|
6
|
+
VERSION_KEY = "_vaezip_version"
|
|
7
|
+
DEFAULT_SHORTCUT = "cast_fp32"
|
|
8
|
+
|
|
9
|
+
BLOCK_QUANT_BITS = (8, 6, 5, 4, 3, 2)
|
|
10
|
+
RANGE_QUANT_BITS = (8, 6, 4)
|
|
11
|
+
BLOCK_QUANT_RANGE_BITS = 4
|
|
12
|
+
BLOCK_QUANT_BLOCK_EXTENT = 8
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _build_shortcut_configs() -> dict[str, dict[str, Any]]:
|
|
16
|
+
configs: dict[str, dict[str, Any]] = {
|
|
17
|
+
DEFAULT_SHORTCUT: {CODEC_CLASS_KEY: "CastCodec", "cast_dtype": "float32"},
|
|
18
|
+
"cast_fp16": {CODEC_CLASS_KEY: "CastCodec", "cast_dtype": "float16"},
|
|
19
|
+
"cast_bf16": {CODEC_CLASS_KEY: "CastCodec", "cast_dtype": "bfloat16"},
|
|
20
|
+
"cast_fp8_e4m3": {CODEC_CLASS_KEY: "CastCodec", "cast_dtype": "float8_e4m3fn"},
|
|
21
|
+
}
|
|
22
|
+
for bits in RANGE_QUANT_BITS:
|
|
23
|
+
configs[f"rq_{bits}bit"] = {
|
|
24
|
+
CODEC_CLASS_KEY: "RangeQuantCodec",
|
|
25
|
+
"bits": bits,
|
|
26
|
+
"channel_dim": 1,
|
|
27
|
+
}
|
|
28
|
+
block_size = [BLOCK_QUANT_BLOCK_EXTENT, BLOCK_QUANT_BLOCK_EXTENT]
|
|
29
|
+
for prefix, class_name in (
|
|
30
|
+
("brq", "BlockRangeQuantCodec"),
|
|
31
|
+
("lbrq", "LloydBlockRangeQuantCodec"),
|
|
32
|
+
):
|
|
33
|
+
for bits in BLOCK_QUANT_BITS:
|
|
34
|
+
configs[f"{prefix}_{bits}bit"] = {
|
|
35
|
+
CODEC_CLASS_KEY: class_name,
|
|
36
|
+
"value_bits": bits,
|
|
37
|
+
"range_bits": BLOCK_QUANT_RANGE_BITS,
|
|
38
|
+
"channel_dim": 1,
|
|
39
|
+
"block_size": list(block_size),
|
|
40
|
+
}
|
|
41
|
+
return configs
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
SHORTCUT_CONFIGS = _build_shortcut_configs()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def list_shortcuts() -> list[str]:
|
|
48
|
+
"""Names of all predefined codec configs."""
|
|
49
|
+
return list(SHORTCUT_CONFIGS)
|
vaezip/encoded_tensor.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Container for compressed latents and its (de)serialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import struct
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
from safetensors.torch import load as load_safetensors
|
|
13
|
+
from safetensors.torch import save as save_safetensors
|
|
14
|
+
|
|
15
|
+
CODEC_CONFIG_KEY = "codec_config"
|
|
16
|
+
SAFETENSORS_FORMAT = "safetensors"
|
|
17
|
+
PTH_FORMAT = "pth"
|
|
18
|
+
SUPPORTED_FORMATS = (SAFETENSORS_FORMAT, PTH_FORMAT)
|
|
19
|
+
|
|
20
|
+
_PTH_TENSORS_KEY = "tensors"
|
|
21
|
+
_PTH_METADATA_KEY = "metadata"
|
|
22
|
+
_SAFETENSORS_HEADER_SIZE_BYTES = 8
|
|
23
|
+
_SAFETENSORS_METADATA_KEY = "__metadata__"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_format(format: str) -> None:
|
|
27
|
+
if format not in SUPPORTED_FORMATS:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"Unsupported format {format!r}, expected one of {SUPPORTED_FORMATS}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class EncodedTensor:
|
|
35
|
+
"""
|
|
36
|
+
Output of `Codec.encode()` and input of `Codec.decode()`.
|
|
37
|
+
|
|
38
|
+
tensors: named tensors with the compressed representation.
|
|
39
|
+
metadata: optional string metadata, normally holds the JSON config of the codec.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
tensors: dict[str, torch.Tensor]
|
|
43
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def codec_config(self) -> dict[str, Any] | None:
|
|
47
|
+
"""Parsed codec config from the metadata, `None` if there is none."""
|
|
48
|
+
raw_config = self.metadata.get(CODEC_CONFIG_KEY)
|
|
49
|
+
if raw_config is None:
|
|
50
|
+
return None
|
|
51
|
+
config = json.loads(raw_config)
|
|
52
|
+
if not isinstance(config, dict):
|
|
53
|
+
raise ValueError("Codec config in the metadata must be a JSON object")
|
|
54
|
+
return config
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def nbytes(self) -> int:
|
|
58
|
+
"""Total size of the stored tensors in bytes."""
|
|
59
|
+
return sum(
|
|
60
|
+
tensor.numel() * tensor.element_size() for tensor in self.tensors.values()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def to(self, device: torch.device | str) -> EncodedTensor:
|
|
64
|
+
return EncodedTensor(
|
|
65
|
+
tensors={name: tensor.to(device) for name, tensor in self.tensors.items()},
|
|
66
|
+
metadata=dict(self.metadata),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def serialize(
|
|
70
|
+
self, save_metadata: bool = True, format: str = SAFETENSORS_FORMAT
|
|
71
|
+
) -> bytes:
|
|
72
|
+
"""
|
|
73
|
+
args:
|
|
74
|
+
save_metadata: store the metadata (including the codec config)
|
|
75
|
+
format: "safetensors" or "pth"
|
|
76
|
+
returns:
|
|
77
|
+
serialized bytes
|
|
78
|
+
"""
|
|
79
|
+
check_format(format)
|
|
80
|
+
metadata = dict(self.metadata) if save_metadata else {}
|
|
81
|
+
cpu_tensors = {
|
|
82
|
+
name: tensor.detach().to("cpu", copy=True).contiguous()
|
|
83
|
+
for name, tensor in self.tensors.items()
|
|
84
|
+
}
|
|
85
|
+
if format == SAFETENSORS_FORMAT:
|
|
86
|
+
return save_safetensors(cpu_tensors, metadata=metadata or None)
|
|
87
|
+
buffer = io.BytesIO()
|
|
88
|
+
torch.save({_PTH_TENSORS_KEY: cpu_tensors, _PTH_METADATA_KEY: metadata}, buffer)
|
|
89
|
+
return buffer.getvalue()
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def deserialize(
|
|
93
|
+
cls, data: bytes, format: str = SAFETENSORS_FORMAT
|
|
94
|
+
) -> EncodedTensor:
|
|
95
|
+
"""
|
|
96
|
+
args:
|
|
97
|
+
data: bytes produced by `serialize()`
|
|
98
|
+
format: "safetensors" or "pth"
|
|
99
|
+
returns:
|
|
100
|
+
encoded tensor with tensors on CPU
|
|
101
|
+
"""
|
|
102
|
+
check_format(format)
|
|
103
|
+
if format == SAFETENSORS_FORMAT:
|
|
104
|
+
return cls(
|
|
105
|
+
tensors=load_safetensors(data),
|
|
106
|
+
metadata=_read_safetensors_metadata(data),
|
|
107
|
+
)
|
|
108
|
+
content = torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
|
|
109
|
+
return cls(
|
|
110
|
+
tensors=dict(content[_PTH_TENSORS_KEY]),
|
|
111
|
+
metadata=dict(content[_PTH_METADATA_KEY]),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def serialize(
|
|
116
|
+
encoded: EncodedTensor,
|
|
117
|
+
save_metadata: bool = True,
|
|
118
|
+
format: str = SAFETENSORS_FORMAT,
|
|
119
|
+
) -> bytes:
|
|
120
|
+
return encoded.serialize(save_metadata=save_metadata, format=format)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def deserialize(data: bytes, format: str = SAFETENSORS_FORMAT) -> EncodedTensor:
|
|
124
|
+
return EncodedTensor.deserialize(data, format=format)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _read_safetensors_metadata(data: bytes) -> dict[str, str]:
|
|
128
|
+
(header_size,) = struct.unpack_from("<Q", data, 0)
|
|
129
|
+
header_end = _SAFETENSORS_HEADER_SIZE_BYTES + header_size
|
|
130
|
+
header = json.loads(bytes(data[_SAFETENSORS_HEADER_SIZE_BYTES:header_end]))
|
|
131
|
+
metadata: dict[str, str] = header.get(_SAFETENSORS_METADATA_KEY, {})
|
|
132
|
+
return metadata
|
vaezip/py.typed
ADDED
|
File without changes
|
vaezip/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tensor helpers shared by the codecs."""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Packing of low-bit integer codes into bytes."""
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
import torch.nn.functional as functional
|
|
5
|
+
|
|
6
|
+
BITS_PER_BYTE = 8
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_bits(bits: int, name: str = "bits") -> None:
|
|
10
|
+
if isinstance(bits, bool) or not isinstance(bits, int):
|
|
11
|
+
raise TypeError(f"{name} must be an int, got {type(bits).__name__}")
|
|
12
|
+
if not 1 <= bits <= BITS_PER_BYTE:
|
|
13
|
+
raise ValueError(f"{name} must be in [1, {BITS_PER_BYTE}], got {bits}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def max_code(bits: int) -> int:
|
|
17
|
+
return (1 << bits) - 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def pack_bits(codes: torch.Tensor, bits: int) -> torch.Tensor:
|
|
21
|
+
"""
|
|
22
|
+
args:
|
|
23
|
+
codes: [B, N], dtype: torch.uint8, values: [0...2^bits - 1]
|
|
24
|
+
bits: number of bits per code, in [1...8]
|
|
25
|
+
returns:
|
|
26
|
+
packed: [B, ceil(N * bits / 8)], dtype: torch.uint8, codes stored LSB first
|
|
27
|
+
"""
|
|
28
|
+
if bits == BITS_PER_BYTE:
|
|
29
|
+
return codes.contiguous()
|
|
30
|
+
batch_size = codes.shape[0]
|
|
31
|
+
bit_shifts = torch.arange(bits, device=codes.device, dtype=torch.uint8)
|
|
32
|
+
bit_planes = ((codes.unsqueeze(-1) >> bit_shifts) & 1).reshape(batch_size, -1)
|
|
33
|
+
bit_planes = functional.pad(bit_planes, (0, (-bit_planes.shape[1]) % BITS_PER_BYTE))
|
|
34
|
+
byte_weights = 1 << torch.arange(
|
|
35
|
+
BITS_PER_BYTE, device=codes.device, dtype=torch.uint8
|
|
36
|
+
)
|
|
37
|
+
byte_groups = bit_planes.reshape(batch_size, -1, BITS_PER_BYTE) * byte_weights
|
|
38
|
+
return byte_groups.sum(dim=-1, dtype=torch.uint8)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def unpack_bits(packed: torch.Tensor, count: int, bits: int) -> torch.Tensor:
|
|
42
|
+
"""
|
|
43
|
+
args:
|
|
44
|
+
packed: [B, ceil(count * bits / 8)], dtype: torch.uint8
|
|
45
|
+
count: number of codes per sample
|
|
46
|
+
bits: number of bits per code, in [1...8]
|
|
47
|
+
returns:
|
|
48
|
+
codes: [B, count], dtype: torch.uint8, values: [0...2^bits - 1]
|
|
49
|
+
"""
|
|
50
|
+
if bits == BITS_PER_BYTE:
|
|
51
|
+
return packed[:, :count]
|
|
52
|
+
batch_size = packed.shape[0]
|
|
53
|
+
bit_shifts = torch.arange(BITS_PER_BYTE, device=packed.device, dtype=torch.uint8)
|
|
54
|
+
bit_planes = ((packed.unsqueeze(-1) >> bit_shifts) & 1).reshape(batch_size, -1)
|
|
55
|
+
bit_planes = bit_planes[:, : count * bits]
|
|
56
|
+
code_weights = 1 << torch.arange(bits, device=packed.device, dtype=torch.uint8)
|
|
57
|
+
code_groups = bit_planes.reshape(batch_size, count, bits) * code_weights
|
|
58
|
+
return code_groups.sum(dim=-1, dtype=torch.uint8)
|
vaezip/utils/blocks.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Splitting spatial dimensions into blocks and merging them back."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def padded_size(size: int, block_extent: int) -> int:
|
|
9
|
+
return math.ceil(size / block_extent) * block_extent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def pad_to_block_multiple(
|
|
13
|
+
tensor: torch.Tensor, block_size: tuple[int, ...]
|
|
14
|
+
) -> torch.Tensor:
|
|
15
|
+
"""
|
|
16
|
+
args:
|
|
17
|
+
tensor: [B, C, *spatial]
|
|
18
|
+
block_size: block extent per spatial dimension
|
|
19
|
+
returns:
|
|
20
|
+
tensor: [B, C, *padded_spatial], spatial sizes divisible by the block size,
|
|
21
|
+
padding repeats edge values so block ranges are not affected
|
|
22
|
+
"""
|
|
23
|
+
for offset, block_extent in enumerate(block_size):
|
|
24
|
+
dim = 2 + offset
|
|
25
|
+
size = tensor.shape[dim]
|
|
26
|
+
target_size = padded_size(size, block_extent)
|
|
27
|
+
if target_size != size:
|
|
28
|
+
source_index = torch.arange(target_size, device=tensor.device)
|
|
29
|
+
tensor = tensor.index_select(dim, source_index.clamp(max=size - 1))
|
|
30
|
+
return tensor
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def split_into_blocks(
|
|
34
|
+
tensor: torch.Tensor, block_size: tuple[int, ...]
|
|
35
|
+
) -> torch.Tensor:
|
|
36
|
+
"""
|
|
37
|
+
args:
|
|
38
|
+
tensor: [B, C, *spatial]
|
|
39
|
+
block_size: block extent per spatial dimension
|
|
40
|
+
returns:
|
|
41
|
+
blocks: [B, C, G, V], G: number of blocks per channel, V: values per block
|
|
42
|
+
"""
|
|
43
|
+
padded = pad_to_block_multiple(tensor, block_size)
|
|
44
|
+
batch_size, channels = padded.shape[:2]
|
|
45
|
+
block_counts = [
|
|
46
|
+
size // extent
|
|
47
|
+
for size, extent in zip(padded.shape[2:], block_size, strict=True)
|
|
48
|
+
]
|
|
49
|
+
split_shape = [
|
|
50
|
+
part
|
|
51
|
+
for count, extent in zip(block_counts, block_size, strict=True)
|
|
52
|
+
for part in (count, extent)
|
|
53
|
+
]
|
|
54
|
+
ndim = len(block_size)
|
|
55
|
+
count_dims = range(2, 2 + 2 * ndim, 2)
|
|
56
|
+
extent_dims = range(3, 3 + 2 * ndim, 2)
|
|
57
|
+
split = padded.reshape(batch_size, channels, *split_shape) # [B, C, n1, b1, n2, b2]
|
|
58
|
+
grouped = split.permute(0, 1, *count_dims, *extent_dims) # [B, C, n1, n2, b1, b2]
|
|
59
|
+
return grouped.reshape(
|
|
60
|
+
batch_size, channels, math.prod(block_counts), math.prod(block_size)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def merge_blocks(
|
|
65
|
+
blocks: torch.Tensor, spatial_shape: list[int], block_size: tuple[int, ...]
|
|
66
|
+
) -> torch.Tensor:
|
|
67
|
+
"""
|
|
68
|
+
args:
|
|
69
|
+
blocks: [B, C, G, V]
|
|
70
|
+
spatial_shape: original spatial sizes before padding
|
|
71
|
+
block_size: block extent per spatial dimension
|
|
72
|
+
returns:
|
|
73
|
+
tensor: [B, C, *spatial_shape]
|
|
74
|
+
"""
|
|
75
|
+
batch_size, channels = blocks.shape[:2]
|
|
76
|
+
block_counts = [
|
|
77
|
+
padded_size(size, extent) // extent
|
|
78
|
+
for size, extent in zip(spatial_shape, block_size, strict=True)
|
|
79
|
+
]
|
|
80
|
+
ndim = len(block_size)
|
|
81
|
+
grouped = blocks.reshape(batch_size, channels, *block_counts, *block_size)
|
|
82
|
+
split_order = [
|
|
83
|
+
dim for index in range(ndim) for dim in (2 + index, 2 + ndim + index)
|
|
84
|
+
]
|
|
85
|
+
split = grouped.permute(0, 1, *split_order) # [B, C, n1, b1, n2, b2]
|
|
86
|
+
padded_shape = [
|
|
87
|
+
count * extent for count, extent in zip(block_counts, block_size, strict=True)
|
|
88
|
+
]
|
|
89
|
+
padded = split.reshape(batch_size, channels, *padded_shape)
|
|
90
|
+
crop = tuple(slice(0, size) for size in spatial_shape)
|
|
91
|
+
return padded[(slice(None), slice(None), *crop)]
|
vaezip/utils/layout.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Tensor layout handling: batch, channel and spatial dimensions."""
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
BATCH_DIM = 0
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_channel_dim(channel_dim: int) -> None:
|
|
9
|
+
if isinstance(channel_dim, bool) or not isinstance(channel_dim, int):
|
|
10
|
+
raise TypeError(f"channel_dim must be an int, got {type(channel_dim).__name__}")
|
|
11
|
+
if channel_dim == BATCH_DIM:
|
|
12
|
+
raise ValueError("channel_dim can not be 0, the first dimension is the batch")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_channel_dim(channel_dim: int, ndim: int) -> int:
|
|
16
|
+
"""Non-negative channel index, negative values count from the end."""
|
|
17
|
+
if not -ndim < channel_dim < ndim or channel_dim % ndim == BATCH_DIM:
|
|
18
|
+
raise ValueError(
|
|
19
|
+
f"channel_dim={channel_dim} is invalid for a tensor with {ndim} dimensions"
|
|
20
|
+
)
|
|
21
|
+
return channel_dim % ndim
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def channel_to_second(tensor: torch.Tensor, channel_dim: int) -> torch.Tensor:
|
|
25
|
+
"""
|
|
26
|
+
args:
|
|
27
|
+
tensor: [B, ...] with the channel at `channel_dim`
|
|
28
|
+
returns:
|
|
29
|
+
tensor: [B, C, ...] view with spatial dimensions in the original order
|
|
30
|
+
"""
|
|
31
|
+
return tensor.movedim(resolve_channel_dim(channel_dim, tensor.ndim), 1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def channel_from_second(tensor: torch.Tensor, channel_dim: int) -> torch.Tensor:
|
|
35
|
+
"""
|
|
36
|
+
args:
|
|
37
|
+
tensor: [B, C, ...]
|
|
38
|
+
returns:
|
|
39
|
+
tensor: [B, ...] view with the channel moved to `channel_dim`
|
|
40
|
+
"""
|
|
41
|
+
return tensor.movedim(1, resolve_channel_dim(channel_dim, tensor.ndim))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def shape_with_channel_second(shape: list[int], channel_dim: int) -> list[int]:
|
|
45
|
+
"""Shape [B, C, ...] of a tensor with the given original shape."""
|
|
46
|
+
channel_index = resolve_channel_dim(channel_dim, len(shape))
|
|
47
|
+
spatial_sizes = [
|
|
48
|
+
size for index, size in enumerate(shape) if index not in (0, channel_index)
|
|
49
|
+
]
|
|
50
|
+
return [shape[0], shape[channel_index], *spatial_sizes]
|