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.
@@ -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)
@@ -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
@@ -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]