entropack 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.
- entropack/__init__.py +15 -0
- entropack/backends/__init__.py +19 -0
- entropack/backends/cuda/__init__.py +16 -0
- entropack/backends/cuda/device.py +96 -0
- entropack/backends/cuda/kernels.py +70 -0
- entropack/compression/__init__.py +4 -0
- entropack/compression/api.py +76 -0
- entropack/compression/compressed_tensor.py +205 -0
- entropack/linear/__init__.py +5 -0
- entropack/linear/linear.py +444 -0
- entropack/linear/quant_kernels.py +108 -0
- entropack/linear/utils.py +41 -0
- entropack/registry.py +121 -0
- entropack/schemes/__init__.py +12 -0
- entropack/schemes/base.py +121 -0
- entropack/schemes/checks.py +33 -0
- entropack/schemes/config.py +111 -0
- entropack/schemes/dfloat11/__init__.py +52 -0
- entropack/schemes/dfloat11/cuda.py +223 -0
- entropack/schemes/dfloat11/dfloat11.cu +740 -0
- entropack/schemes/dfloat11/eager.py +239 -0
- entropack/schemes/dfloat11/format.py +146 -0
- entropack/schemes/lattice_rans/__init__.py +60 -0
- entropack/schemes/lattice_rans/cuda.py +670 -0
- entropack/schemes/lattice_rans/eager.py +405 -0
- entropack/schemes/lattice_rans/format.py +309 -0
- entropack/schemes/lattice_rans/lattice_rans.cu +1140 -0
- entropack/schemes/lattice_rans/rans.py +209 -0
- entropack/schemes/lattice_rans/rdo.py +162 -0
- entropack/schemes/tile_ans/__init__.py +55 -0
- entropack/schemes/tile_ans/cuda.py +170 -0
- entropack/schemes/tile_ans/device.cuh +90 -0
- entropack/schemes/tile_ans/eager.py +307 -0
- entropack/schemes/tile_ans/format.py +164 -0
- entropack/schemes/tile_ans/tile_ans.cu +469 -0
- entropack-0.1.0.dist-info/METADATA +158 -0
- entropack-0.1.0.dist-info/RECORD +40 -0
- entropack-0.1.0.dist-info/WHEEL +5 -0
- entropack-0.1.0.dist-info/licenses/LICENSE +201 -0
- entropack-0.1.0.dist-info/top_level.txt +1 -0
entropack/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from importlib import metadata
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = metadata.version("entropack")
|
|
5
|
+
except metadata.PackageNotFoundError:
|
|
6
|
+
__version__ = "0+unknown"
|
|
7
|
+
|
|
8
|
+
from .compression import CompressedTensor, compress, decompress
|
|
9
|
+
from .linear import CompressedFP8Linear, CompressedINT8Linear, CompressedLinear
|
|
10
|
+
from .schemes import CompressionConfig, DFloat11Config, LatticeRANSConfig, RawConfig, TileANSConfig
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CompressedFP8Linear", "CompressedINT8Linear", "CompressedLinear", "CompressedTensor", "CompressionConfig",
|
|
14
|
+
"DFloat11Config", "LatticeRANSConfig", "RawConfig", "TileANSConfig", "__version__", "compress", "decompress",
|
|
15
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from . import cuda
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Backend:
|
|
11
|
+
name: str
|
|
12
|
+
priority: int = 0
|
|
13
|
+
probe: Callable[[], str | None] | None = None
|
|
14
|
+
device: Callable[[], torch.device] | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
BACKENDS: tuple[Backend, ...] = (
|
|
18
|
+
Backend("cuda", priority=100, probe=cuda.probe, device=cuda.runs_on), Backend("eager", priority=0),
|
|
19
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def probe() -> str | None:
|
|
5
|
+
if not torch.cuda.is_available():
|
|
6
|
+
return "torch.cuda.is_available() is False (no NVIDIA device / driver)"
|
|
7
|
+
try:
|
|
8
|
+
import cupy
|
|
9
|
+
except ImportError as e:
|
|
10
|
+
return (f"cupy is not installed ({e}); install a matching build, e.g. "
|
|
11
|
+
"`pip install cupy-cuda13x` for CUDA 13 or `pip install cupy-cuda12x` for CUDA 12")
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def runs_on() -> torch.device:
|
|
16
|
+
return torch.device("cuda", torch.cuda.current_device())
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import cupy
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from .kernels import device_index
|
|
7
|
+
|
|
8
|
+
_FALLBACK_WARP_SIZE = 32
|
|
9
|
+
|
|
10
|
+
_DEFAULT_GRID_WAVES = 8
|
|
11
|
+
|
|
12
|
+
__all__ = ["DeviceCaps", "caps", "resolve_threads", "validate_threads_per_block"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _shared_optin(index: int, properties) -> int:
|
|
16
|
+
optin = getattr(properties, "shared_memory_per_block_optin", 0)
|
|
17
|
+
if optin:
|
|
18
|
+
return int(optin)
|
|
19
|
+
with cupy.cuda.Device(index):
|
|
20
|
+
attributes = cupy.cuda.Device(index).attributes
|
|
21
|
+
return int(attributes["MaxSharedMemoryPerBlockOptin"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class DeviceCaps:
|
|
26
|
+
index: int
|
|
27
|
+
name: str
|
|
28
|
+
compute_capability: tuple[int, int]
|
|
29
|
+
sm_count: int
|
|
30
|
+
warp_size: int
|
|
31
|
+
max_threads_per_block: int
|
|
32
|
+
threads_per_sm: int
|
|
33
|
+
regs_per_sm: int
|
|
34
|
+
shared_per_block: int
|
|
35
|
+
shared_optin: int
|
|
36
|
+
shared_per_sm: int
|
|
37
|
+
l2_bytes: int
|
|
38
|
+
|
|
39
|
+
def blocks_per_sm(self, threads_per_block: int, shared_per_block: int = 0, regs_per_thread: int = 0) -> int:
|
|
40
|
+
blocks = self.threads_per_sm // threads_per_block
|
|
41
|
+
if shared_per_block > 0:
|
|
42
|
+
blocks = min(blocks, self.shared_per_sm // shared_per_block)
|
|
43
|
+
if regs_per_thread > 0:
|
|
44
|
+
blocks = min(blocks, self.regs_per_sm // (regs_per_thread * threads_per_block))
|
|
45
|
+
return max(1, blocks)
|
|
46
|
+
|
|
47
|
+
def resident_blocks(self, threads_per_block: int, shared_per_block: int = 0, regs_per_thread: int = 0) -> int:
|
|
48
|
+
return self.sm_count * self.blocks_per_sm(threads_per_block, shared_per_block, regs_per_thread)
|
|
49
|
+
|
|
50
|
+
def grid(self, wanted: int, threads_per_block: int, shared_per_block: int = 0, waves: int = _DEFAULT_GRID_WAVES) -> int:
|
|
51
|
+
limit = self.resident_blocks(threads_per_block, shared_per_block) * waves
|
|
52
|
+
return max(1, min(wanted, limit))
|
|
53
|
+
|
|
54
|
+
def shared_limit(self, static_slack: int = 0) -> int:
|
|
55
|
+
return max(0, min(self.shared_optin, self.shared_per_sm - static_slack))
|
|
56
|
+
|
|
57
|
+
def threads_per_block(self, wanted: int) -> int:
|
|
58
|
+
usable = min(wanted, self.max_threads_per_block)
|
|
59
|
+
usable -= usable % self.warp_size
|
|
60
|
+
return max(self.warp_size, usable)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_caps_cache: dict[int, DeviceCaps] = {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def caps(device=None) -> DeviceCaps:
|
|
67
|
+
index = device_index(device)
|
|
68
|
+
cached = _caps_cache.get(index)
|
|
69
|
+
if cached is not None:
|
|
70
|
+
return cached
|
|
71
|
+
properties = torch.cuda.get_device_properties(index)
|
|
72
|
+
queried = DeviceCaps(
|
|
73
|
+
index=index, name=properties.name, compute_capability=(properties.major, properties.minor),
|
|
74
|
+
sm_count=properties.multi_processor_count, warp_size=getattr(properties, "warp_size", 0) or _FALLBACK_WARP_SIZE,
|
|
75
|
+
max_threads_per_block=properties.max_threads_per_block, threads_per_sm=properties.max_threads_per_multi_processor,
|
|
76
|
+
regs_per_sm=properties.regs_per_multiprocessor, shared_per_block=properties.shared_memory_per_block,
|
|
77
|
+
shared_optin=_shared_optin(index, properties), shared_per_sm=properties.shared_memory_per_multiprocessor,
|
|
78
|
+
l2_bytes=getattr(properties, "L2_cache_size", 0),
|
|
79
|
+
)
|
|
80
|
+
_caps_cache[index] = queried
|
|
81
|
+
return queried
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def validate_threads_per_block(caps: DeviceCaps, threads_per_block: int) -> None:
|
|
85
|
+
if threads_per_block % caps.warp_size or threads_per_block > caps.max_threads_per_block:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"threads_per_block={threads_per_block} is not launchable on {caps.name}: it must be "
|
|
88
|
+
f"a multiple of the {caps.warp_size}-thread warp size and at most {caps.max_threads_per_block}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def resolve_threads(caps: DeviceCaps, requested: int | None, default: int) -> int:
|
|
93
|
+
if requested is None:
|
|
94
|
+
return caps.threads_per_block(default)
|
|
95
|
+
validate_threads_per_block(caps, requested)
|
|
96
|
+
return requested
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from collections.abc import Callable, Hashable, Sequence
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import cupy
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
_modules: dict[tuple, object] = {}
|
|
9
|
+
_kernels: dict[tuple, object] = {}
|
|
10
|
+
_streams: dict[tuple, object] = {}
|
|
11
|
+
|
|
12
|
+
__all__ = ["KernelLibrary", "device_index", "ensure_dynamic_shared", "external_stream", "pointer"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ensure_dynamic_shared(kernel, shared_bytes: int) -> None:
|
|
16
|
+
if shared_bytes > kernel.max_dynamic_shared_size_bytes:
|
|
17
|
+
kernel.max_dynamic_shared_size_bytes = shared_bytes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KernelLibrary:
|
|
21
|
+
def __init__(
|
|
22
|
+
self, key: str, source: Path, defines: Callable[[int, Hashable], Sequence[str]],
|
|
23
|
+
includes: Sequence[Path] = (), kernel_names: Sequence[str] | None = None,
|
|
24
|
+
):
|
|
25
|
+
self.key = key
|
|
26
|
+
self.source = source
|
|
27
|
+
self.defines = defines
|
|
28
|
+
self.includes = tuple(includes)
|
|
29
|
+
self.kernel_names = None if kernel_names is None else frozenset(kernel_names)
|
|
30
|
+
|
|
31
|
+
def kernel(self, device_index: int, variant: Hashable, name: str):
|
|
32
|
+
if self.kernel_names is not None and name not in self.kernel_names:
|
|
33
|
+
raise KeyError(name)
|
|
34
|
+
module_key = (self.key, device_index, variant)
|
|
35
|
+
module = _modules.get(module_key)
|
|
36
|
+
if module is None:
|
|
37
|
+
options = ["--std=c++17"]
|
|
38
|
+
options += [f"-D{definition}" for definition in self.defines(device_index, variant)]
|
|
39
|
+
options += [f"-I{include}" for include in self.includes]
|
|
40
|
+
with cupy.cuda.Device(device_index):
|
|
41
|
+
module = cupy.RawModule(code=self.source.read_text(), options=tuple(options))
|
|
42
|
+
_modules[module_key] = module
|
|
43
|
+
kernel_key = (self.key, device_index, variant, name)
|
|
44
|
+
kernel = _kernels.get(kernel_key)
|
|
45
|
+
if kernel is None:
|
|
46
|
+
kernel = module.get_function(name)
|
|
47
|
+
_kernels[kernel_key] = kernel
|
|
48
|
+
return kernel
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def external_stream(stream: torch.cuda.Stream):
|
|
52
|
+
key = (device_index(stream.device), int(stream.cuda_stream))
|
|
53
|
+
wrapper = _streams.get(key)
|
|
54
|
+
if wrapper is None:
|
|
55
|
+
factory = getattr(cupy.cuda.Stream, "from_external", None)
|
|
56
|
+
wrapper = factory(stream) if factory else cupy.cuda.ExternalStream(key[1])
|
|
57
|
+
_streams[key] = wrapper
|
|
58
|
+
return wrapper
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def pointer(tensor: torch.Tensor) -> np.uint64:
|
|
62
|
+
return np.uint64(tensor.data_ptr())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def device_index(target) -> int:
|
|
66
|
+
if isinstance(target, int):
|
|
67
|
+
return target
|
|
68
|
+
device = target.device if isinstance(target, torch.Tensor) else target
|
|
69
|
+
index = getattr(device, "index", None)
|
|
70
|
+
return torch.cuda.current_device() if index is None else index
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
import torch
|
|
5
|
+
|
|
6
|
+
from ..registry import DispatchError, get_scheme, name_for_config
|
|
7
|
+
from ..schemes import CompressionConfig, RawConfig
|
|
8
|
+
from ..schemes.config import validate_config
|
|
9
|
+
from .compressed_tensor import CompressedTensor
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("entropack")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CompressionFallbackWarning(RuntimeWarning):
|
|
15
|
+
"""Warning emitted when compression falls back to storing the tensor uncompressed."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compress(tensor: torch.Tensor, config: CompressionConfig) -> CompressedTensor:
|
|
19
|
+
"""Compress a tensor using the selected scheme.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
tensor: input values. The container preserves the input shape, dtype, and device.
|
|
23
|
+
config: configuration selecting the compression scheme and execution backend.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A :class:`CompressedTensor` containing the encoded data and metadata.
|
|
27
|
+
|
|
28
|
+
Encoding failures can return an uncompressed ``raw`` container with a
|
|
29
|
+
:class:`CompressionFallbackWarning`. Its header records the requested scheme and
|
|
30
|
+
failure reason. Invalid configurations and backend dispatch failures raise errors.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(tensor, CompressedTensor):
|
|
33
|
+
raise TypeError("compress expects an uncompressed tensor; decompress the container before recompressing")
|
|
34
|
+
scheme = get_scheme(name_for_config(config))
|
|
35
|
+
validate_config(config)
|
|
36
|
+
try:
|
|
37
|
+
packed = scheme.encode(tensor, config)
|
|
38
|
+
except DispatchError:
|
|
39
|
+
raise
|
|
40
|
+
except Exception as error:
|
|
41
|
+
logger.warning(
|
|
42
|
+
"%s could not encode a %s %s tensor, storing it uncompressed: %s",
|
|
43
|
+
scheme.name, tuple(tensor.shape), tensor.dtype, error,
|
|
44
|
+
)
|
|
45
|
+
return _compress_raw(tensor, scheme.name, error)
|
|
46
|
+
return CompressedTensor(header={"compress_method": scheme.name}, buffers=packed, shape=tuple(tensor.shape), dtype=tensor.dtype)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _compress_raw(tensor: torch.Tensor, requested: str, error: Exception) -> CompressedTensor:
|
|
50
|
+
reason = f"{type(error).__name__}: {error}"
|
|
51
|
+
warnings.warn(
|
|
52
|
+
f"entropack stored a {tuple(tensor.shape)} {tensor.dtype} tensor uncompressed, at "
|
|
53
|
+
f"{tensor.element_size() * 8} bits per element: compress_method={requested!r} failed with {reason}",
|
|
54
|
+
CompressionFallbackWarning, stacklevel=3,
|
|
55
|
+
)
|
|
56
|
+
return CompressedTensor(
|
|
57
|
+
header={"compress_method": "raw", "requested": requested, "reason": reason},
|
|
58
|
+
buffers=get_scheme("raw").encode(tensor, RawConfig()), shape=tuple(tensor.shape), dtype=tensor.dtype,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def decompress(compressed: CompressedTensor, config: CompressionConfig) -> torch.Tensor:
|
|
63
|
+
"""Restore a tensor from its compressed representation.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
compressed: container to decode. Its header identifies the compression scheme.
|
|
67
|
+
config: configuration for the same scheme. Decode settings select the backend and
|
|
68
|
+
execution options. Encode settings such as the target bitrate do not recompress
|
|
69
|
+
the stored data.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A tensor with the container's shape and dtype, on the device holding its buffers.
|
|
73
|
+
"""
|
|
74
|
+
validate_config(config)
|
|
75
|
+
restored = compressed.scheme.decode(compressed.buffers, shape=compressed.shape, dtype=compressed.encoded_dtype, config=config)
|
|
76
|
+
return restored.to(compressed.dtype)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from torch.utils._python_dispatch import return_and_correct_aliasing
|
|
9
|
+
|
|
10
|
+
from ..registry import get_scheme
|
|
11
|
+
from ..schemes import Scheme
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_dtype(name: str) -> torch.dtype:
|
|
15
|
+
if not name:
|
|
16
|
+
raise TypeError("CompressedTensor serialized dtype must be a non-empty string")
|
|
17
|
+
dtype = getattr(torch, name, None)
|
|
18
|
+
if not isinstance(dtype, torch.dtype):
|
|
19
|
+
raise ValueError(f"Unsupported serialized torch dtype '{name}'")
|
|
20
|
+
return dtype
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CompressedTensor(torch.Tensor):
|
|
24
|
+
"""Frozen compressed tensor with a logical output dtype.
|
|
25
|
+
|
|
26
|
+
``encoded_dtype`` records the codec's input type. ``to(dtype=...)`` changes
|
|
27
|
+
the output dtype without casting packed buffers or re-encoding values.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
header: dict[str, Any]
|
|
31
|
+
buffers: dict[str, torch.Tensor]
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def __new__(cls, header, buffers, shape, dtype, *, encoded_dtype=None):
|
|
35
|
+
device = next(iter(buffers.values())).device
|
|
36
|
+
return torch.Tensor._make_wrapper_subclass(cls, tuple(shape), dtype=dtype, device=device, requires_grad=False)
|
|
37
|
+
|
|
38
|
+
def __init__(self, header, buffers, shape, dtype, *, encoded_dtype=None):
|
|
39
|
+
self.header = copy.deepcopy(header)
|
|
40
|
+
self.buffers = dict(buffers)
|
|
41
|
+
self.encoded_dtype = dtype if encoded_dtype is None else encoded_dtype
|
|
42
|
+
self.validate()
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
return f"{type(self).__name__}(shape={tuple(self.shape)}, dtype={self.dtype}, device={self.device}, scheme={self.compress_method!r})"
|
|
46
|
+
|
|
47
|
+
def __tensor_flatten__(self):
|
|
48
|
+
names = tuple(self.buffers)
|
|
49
|
+
for name, value in self.buffers.items():
|
|
50
|
+
setattr(self, "_packed_" + name, value)
|
|
51
|
+
metadata = (names, copy.deepcopy(self.header), tuple(self.shape), self.dtype, self.encoded_dtype)
|
|
52
|
+
return ["_packed_" + name for name in names], metadata
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def __tensor_unflatten__(cls, inner_tensors, metadata, outer_size, outer_stride):
|
|
56
|
+
names, header, shape, dtype, encoded_dtype = metadata
|
|
57
|
+
return cls(
|
|
58
|
+
header=header, buffers={name: inner_tensors["_packed_" + name] for name in names},
|
|
59
|
+
shape=shape, dtype=dtype, encoded_dtype=encoded_dtype,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _map_buffers(self, fn, *, dtype=None) -> "CompressedTensor":
|
|
63
|
+
return type(self)(
|
|
64
|
+
header=self.header, buffers={name: fn(value) for name, value in self.buffers.items()},
|
|
65
|
+
shape=self.shape, dtype=self.dtype if dtype is None else dtype, encoded_dtype=self.encoded_dtype,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def to(self, *args, copy: bool = False, **kwargs) -> "CompressedTensor":
|
|
69
|
+
"""Move buffers or change the logical dtype, with an optional keyword-only copy."""
|
|
70
|
+
device, dtype, non_blocking, memory_format = torch._C._nn._parse_to(*args, **kwargs)
|
|
71
|
+
result = super().to(device=device, dtype=dtype, non_blocking=non_blocking, copy=copy, memory_format=memory_format)
|
|
72
|
+
return result.clone() if copy and result.device == self.device else result
|
|
73
|
+
|
|
74
|
+
def __copy__(self) -> "CompressedTensor":
|
|
75
|
+
result = self._map_buffers(lambda value: value)
|
|
76
|
+
if getattr(self, "_is_param", False):
|
|
77
|
+
result._is_param = True
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
def __deepcopy__(self, memo) -> "CompressedTensor":
|
|
81
|
+
if id(self) in memo:
|
|
82
|
+
return memo[id(self)]
|
|
83
|
+
result = self._map_buffers(lambda value: copy.deepcopy(value, memo))
|
|
84
|
+
if getattr(self, "_is_param", False):
|
|
85
|
+
result._is_param = True
|
|
86
|
+
memo[id(self)] = result
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
def requires_grad_(self, requires_grad: bool = False):
|
|
90
|
+
"""Compressed values are frozen; gradients may flow through their consumers."""
|
|
91
|
+
if requires_grad:
|
|
92
|
+
raise RuntimeError("CompressedTensor cannot require gradients; decompress it to train dense values")
|
|
93
|
+
return torch.Tensor.requires_grad_(self, False)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def __torch_dispatch__(cls, func, types, args=(), kwargs=None):
|
|
97
|
+
kwargs = kwargs or {}
|
|
98
|
+
source = args[0] if args else None
|
|
99
|
+
aten = torch.ops.aten
|
|
100
|
+
|
|
101
|
+
if func in (aten.detach.default, aten.alias.default):
|
|
102
|
+
result = source._map_buffers(torch.detach)
|
|
103
|
+
return return_and_correct_aliasing(func, args, kwargs, result)
|
|
104
|
+
|
|
105
|
+
if func in (aten.clone.default, aten._to_copy.default):
|
|
106
|
+
if kwargs.get("memory_format") not in (None, torch.preserve_format, torch.contiguous_format):
|
|
107
|
+
raise ValueError("CompressedTensor does not support that memory format")
|
|
108
|
+
if func == aten.clone.default:
|
|
109
|
+
return source._map_buffers(torch.clone)
|
|
110
|
+
if kwargs.get("layout", torch.strided) not in (None, torch.strided):
|
|
111
|
+
raise ValueError("CompressedTensor only supports strided storage")
|
|
112
|
+
options = {name: value for name, value in kwargs.items() if name in ("device", "non_blocking")}
|
|
113
|
+
return source._map_buffers(lambda value: value.to(**options), dtype=kwargs.get("dtype"))
|
|
114
|
+
|
|
115
|
+
result = func.decompose(*args, **kwargs)
|
|
116
|
+
if result is not NotImplemented:
|
|
117
|
+
return result
|
|
118
|
+
raise NotImplementedError(f"CompressedTensor does not implement {func}; decompress it before numerical operations")
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def compress_method(self) -> str:
|
|
122
|
+
"""The scheme name the header carries."""
|
|
123
|
+
return self.header["compress_method"]
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def scheme(self) -> Scheme:
|
|
127
|
+
"""The codec named by the header."""
|
|
128
|
+
return get_scheme(self.compress_method)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def lossless(self) -> bool:
|
|
132
|
+
"""Whether the encoding scheme is lossless, before any output dtype conversion."""
|
|
133
|
+
return self.scheme.lossless
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def actual_bpp(self) -> float:
|
|
137
|
+
"""Bits per element of :attr:`shape`, serialized header included."""
|
|
138
|
+
return self.storage_nbytes() * 8 / math.prod(self.shape)
|
|
139
|
+
|
|
140
|
+
def validate(self) -> None:
|
|
141
|
+
"""Check the scheme, dtype, and buffer names without inspecting contents."""
|
|
142
|
+
if any(isinstance(value, CompressedTensor) for value in self.buffers.values()):
|
|
143
|
+
raise TypeError("CompressedTensor buffers cannot contain another CompressedTensor")
|
|
144
|
+
scheme = self.scheme
|
|
145
|
+
if not scheme.supports(self.encoded_dtype):
|
|
146
|
+
raise ValueError(f"'{scheme.name}' does not support format {self.encoded_dtype}")
|
|
147
|
+
if set(self.buffers) != set(scheme.buffer_names):
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"CompressedTensor buffers for {self.compress_method} must be "
|
|
150
|
+
f"{list(scheme.buffer_names)}, got {sorted(self.buffers)}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _serialized_header_tensor(self) -> torch.Tensor:
|
|
154
|
+
metadata = {
|
|
155
|
+
"compress_method": self.compress_method, "dtype": str(self.encoded_dtype).removeprefix("torch."),
|
|
156
|
+
"shape": list(self.shape), "buffer_names": list(self.buffers), "header": self.header,
|
|
157
|
+
}
|
|
158
|
+
encoded = json.dumps(metadata, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
|
159
|
+
return torch.frombuffer(bytearray(encoded), dtype=torch.uint8)
|
|
160
|
+
|
|
161
|
+
def storage_nbytes(self, include_header: bool = True) -> int:
|
|
162
|
+
"""Bytes the buffers occupy, plus the serialized header unless ``include_header`` is false."""
|
|
163
|
+
total = sum(value.numel() * value.element_size() for value in self.buffers.values())
|
|
164
|
+
if include_header:
|
|
165
|
+
total += self._serialized_header_tensor().numel()
|
|
166
|
+
return total
|
|
167
|
+
|
|
168
|
+
def state_dict(self, prefix: str = "") -> dict[str, torch.Tensor]:
|
|
169
|
+
"""The container as flat tensors under ``prefix``: one 1D uint8 header, then one entry per buffer."""
|
|
170
|
+
self.validate()
|
|
171
|
+
state = {f"{prefix}header": self._serialized_header_tensor()}
|
|
172
|
+
state.update({f"{prefix}buffers.{name}": value.detach() for name, value in self.buffers.items()})
|
|
173
|
+
return state
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def from_state_dict(cls, state: Mapping[str, torch.Tensor], prefix: str = "") -> "CompressedTensor":
|
|
177
|
+
"""Restore a container in its encoded dtype from a tensor-only checkpoint."""
|
|
178
|
+
header_key = f"{prefix}header"
|
|
179
|
+
if header_key not in state:
|
|
180
|
+
raise ValueError(f"CompressedTensor state is missing '{header_key}'")
|
|
181
|
+
header_tensor = state[header_key]
|
|
182
|
+
if header_tensor.dtype != torch.uint8 or header_tensor.ndim != 1:
|
|
183
|
+
raise TypeError("CompressedTensor serialized header must be a 1D uint8 tensor")
|
|
184
|
+
try:
|
|
185
|
+
metadata = json.loads(bytes(header_tensor.detach().cpu().tolist()).decode("utf-8"))
|
|
186
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
187
|
+
raise ValueError("CompressedTensor serialized header is invalid") from error
|
|
188
|
+
if not isinstance(metadata, dict):
|
|
189
|
+
raise ValueError("CompressedTensor serialized header must contain a JSON object")
|
|
190
|
+
|
|
191
|
+
required = {"compress_method", "dtype", "shape", "buffer_names", "header"}
|
|
192
|
+
missing = required - metadata.keys()
|
|
193
|
+
if missing:
|
|
194
|
+
raise ValueError(f"CompressedTensor serialized header is missing fields: {sorted(missing)}")
|
|
195
|
+
buffer_names = metadata["buffer_names"]
|
|
196
|
+
if not isinstance(buffer_names, list) or any(not isinstance(name, str) or not name for name in buffer_names):
|
|
197
|
+
raise TypeError("CompressedTensor buffer_names must be a list of strings")
|
|
198
|
+
buffers = {}
|
|
199
|
+
for name in buffer_names:
|
|
200
|
+
key = f"{prefix}buffers.{name}"
|
|
201
|
+
if key not in state:
|
|
202
|
+
raise ValueError(f"CompressedTensor state is missing '{key}'")
|
|
203
|
+
buffers[name] = state[key]
|
|
204
|
+
|
|
205
|
+
return cls(header=metadata["header"], buffers=buffers, shape=tuple(metadata["shape"]), dtype=_parse_dtype(metadata["dtype"]))
|