unifiedefficientloader 0.5.2__tar.gz → 0.5.4__tar.gz

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.
Files changed (35) hide show
  1. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/PKG-INFO +2 -2
  2. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/pyproject.toml +2 -2
  3. unifiedefficientloader-0.5.4/tests/test_low_bit_dtypes.py +83 -0
  4. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/tests/test_mmap.py +23 -0
  5. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/incremental_writer.py +8 -6
  6. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/memory_efficient_loader.py +6 -6
  7. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/tensor_utils.py +29 -1
  8. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/uel/model_mmap.py +6 -0
  9. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader.egg-info/PKG-INFO +2 -2
  10. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader.egg-info/SOURCES.txt +1 -0
  11. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader.egg-info/requires.txt +1 -1
  12. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/LICENSE +0 -0
  13. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/README.md +0 -0
  14. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/setup.cfg +0 -0
  15. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/setup.py +0 -0
  16. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/tests/test_direct_gpu.py +0 -0
  17. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/tests/test_incremental_writer.py +0 -0
  18. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/tests/test_logging.py +0 -0
  19. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/tests/test_mmap_diagnostics.py +0 -0
  20. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/tests/test_unified_data_loader.py +0 -0
  21. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/tests/test_utils.py +0 -0
  22. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/__init__.py +0 -0
  23. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/gpu_buffer_pool.py +0 -0
  24. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/logging_utils.py +0 -0
  25. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/pinned_buffer_pool.py +0 -0
  26. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/pinned_transfer.py +0 -0
  27. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/uel/__init__.py +0 -0
  28. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/uel/control.py +0 -0
  29. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/uel/host_buffer.py +0 -0
  30. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/uel/model_vbar.py +0 -0
  31. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/uel/torch.py +0 -0
  32. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/uel/vram_buffer.py +0 -0
  33. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader/unified_data_loader.py +0 -0
  34. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader.egg-info/dependency_links.txt +0 -0
  35. {unifiedefficientloader-0.5.2 → unifiedefficientloader-0.5.4}/unifiedefficientloader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifiedefficientloader
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: A unified interface for memory efficient per tensor loading of safetensors files as raw bytes from offset, handling CPU/GPU pinned transfers, and converting between tensors and dicts.
5
5
  Author: silveroxides
6
6
  License: MIT License
@@ -33,7 +33,7 @@ Classifier: Operating System :: POSIX :: Linux
33
33
  Requires-Python: >=3.9
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
- Requires-Dist: comfy-aimdo>=0.4.5
36
+ Requires-Dist: comfy-aimdo>=0.5.1
37
37
  Provides-Extra: torch
38
38
  Requires-Dist: torch; extra == "torch"
39
39
  Provides-Extra: safetensors
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unifiedefficientloader"
7
- version = "0.5.2"
7
+ version = "0.5.4"
8
8
  description = "A unified interface for memory efficient per tensor loading of safetensors files as raw bytes from offset, handling CPU/GPU pinned transfers, and converting between tensors and dicts."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "Operating System :: POSIX :: Linux",
20
20
  ]
21
21
  requires-python = ">=3.9"
22
- dependencies = ["comfy-aimdo>=0.4.5"]
22
+ dependencies = ["comfy-aimdo>=0.5.1"]
23
23
 
24
24
  [project.optional-dependencies]
25
25
  torch = ["torch"]
@@ -0,0 +1,83 @@
1
+ import json
2
+ import struct
3
+
4
+ import pytest
5
+ import torch
6
+ from safetensors.torch import load_file, save_file
7
+
8
+ from unifiedefficientloader import IncrementalSafetensorsWriter, UnifiedSafetensorsLoader
9
+ from unifiedefficientloader.tensor_utils import get_dtype_size, st_shape_to_torch_shape, st_to_torch_dtype, torch_shape_to_st_shape, torch_to_st_dtype
10
+
11
+ LOW_BIT_DTYPES = [
12
+ ("float8_e4m3fn", "F8_E4M3"),
13
+ ("float8_e5m2", "F8_E5M2"),
14
+ ("float8_e8m0fnu", "F8_E8M0"),
15
+ ("float4_e2m1fn_x2", "F4"),
16
+ ]
17
+
18
+
19
+ def _dtype_or_skip(name):
20
+ dtype = getattr(torch, name, None)
21
+ if dtype is None:
22
+ pytest.skip(f"Installed PyTorch has no torch.{name}")
23
+ return dtype
24
+
25
+
26
+ def _payload(path, key="low"):
27
+ with open(path, "rb") as source:
28
+ header_size = struct.unpack("<Q", source.read(8))[0]
29
+ header = json.loads(source.read(header_size).decode("utf-8"))
30
+ start, end = header[key]["data_offsets"]
31
+ source.seek(8 + header_size + start)
32
+ return header[key], source.read(end - start)
33
+
34
+
35
+ @pytest.mark.parametrize(("torch_name", "storage_code"), LOW_BIT_DTYPES)
36
+ def test_low_bit_dtype_mappings(torch_name, storage_code):
37
+ dtype = _dtype_or_skip(torch_name)
38
+ assert torch_to_st_dtype(dtype) == storage_code
39
+ assert st_to_torch_dtype(storage_code) == dtype
40
+ assert get_dtype_size(storage_code) == 1
41
+
42
+
43
+ def test_packed_fp4_shape_conversion():
44
+ assert torch_shape_to_st_shape((2, 3, 4), "F4") == [2, 3, 8]
45
+ assert st_shape_to_torch_shape((2, 3, 8), "F4") == (2, 3, 4)
46
+ with pytest.raises(ValueError, match="at least one dimension"):
47
+ torch_shape_to_st_shape((), "F4")
48
+ with pytest.raises(ValueError, match="Invalid packed F4"):
49
+ st_shape_to_torch_shape((2, 3, 7), "F4")
50
+
51
+
52
+ @pytest.mark.parametrize(("torch_name", "storage_code"), LOW_BIT_DTYPES)
53
+ @pytest.mark.parametrize("write_mode", ["single", "batch"])
54
+ def test_low_bit_streaming_and_writer_round_trip(tmp_path, torch_name, storage_code, write_mode):
55
+ dtype = _dtype_or_skip(torch_name)
56
+ source_path = tmp_path / f"source_{storage_code}.safetensors"
57
+ output_path = tmp_path / f"output_{storage_code}_{write_mode}.safetensors"
58
+ original = torch.zeros((2, 4), dtype=dtype)
59
+ try:
60
+ save_file({"low": original}, str(source_path))
61
+ except (KeyError, RuntimeError, TypeError, ValueError):
62
+ pytest.skip(f"Installed safetensors cannot write {dtype}")
63
+
64
+ with UnifiedSafetensorsLoader(str(source_path), low_memory=True) as loader:
65
+ assert loader.get_dtype("low") == dtype
66
+ assert loader.get_shape("low") == original.shape
67
+ loaded = loader.get_tensor("low")
68
+ assert loaded.dtype == dtype
69
+ assert loaded.shape == original.shape
70
+ with IncrementalSafetensorsWriter(str(output_path)) as writer:
71
+ if write_mode == "single":
72
+ writer.write("low", loaded)
73
+ else:
74
+ writer.write_batch([("low", loaded)])
75
+
76
+ source_header, source_payload = _payload(source_path)
77
+ output_header, output_payload = _payload(output_path)
78
+ assert source_header["dtype"] == output_header["dtype"] == storage_code
79
+ assert source_header["shape"] == output_header["shape"]
80
+ assert source_payload == output_payload
81
+ round_tripped = load_file(str(output_path))["low"]
82
+ assert round_tripped.dtype == dtype
83
+ assert round_tripped.shape == original.shape
@@ -106,6 +106,29 @@ def test_mmap_get_tensor_values(sample_file):
106
106
  assert torch.equal(t.clone(), expected), f"{key}: value mismatch"
107
107
 
108
108
 
109
+ @uel_required
110
+ def test_mmap_exposes_file_handle(sample_file):
111
+ """The compatibility wrapper exposes comfy-aimdo's file handle API."""
112
+ path, _ = sample_file
113
+
114
+ with UnifiedSafetensorsLoader(path, low_memory=True, use_mmap=True) as loader:
115
+ file_handle = loader._mmap.get_file_handle()
116
+
117
+ assert isinstance(file_handle, int)
118
+ assert file_handle > 0
119
+
120
+
121
+ def test_control_init_accepts_aimdo_051_options():
122
+ """The control wrapper forwards comfy-aimdo 0.5.1 init options."""
123
+ from unifiedefficientloader.uel import control
124
+
125
+ assert control.init(
126
+ implementation=None,
127
+ simple_vram_headroom=None,
128
+ nvml_pressure=False,
129
+ )
130
+
131
+
109
132
  @uel_required
110
133
  def test_mmap_get_tensor_mmap_ref_attached(sample_file):
111
134
  """
@@ -16,7 +16,7 @@ import ctypes
16
16
  from concurrent.futures import ThreadPoolExecutor
17
17
 
18
18
  from . import logging_utils
19
- from .tensor_utils import torch_to_st_dtype, get_dtype_size
19
+ from .tensor_utils import get_dtype_size, torch_shape_to_st_shape, torch_to_st_dtype
20
20
  logger = logging_utils.get_logger(__name__)
21
21
 
22
22
  def _ensure_torch():
@@ -211,8 +211,9 @@ class IncrementalSafetensorsWriter:
211
211
  prepared = []
212
212
  for name, tensor in batch:
213
213
  st_dtype = torch_to_st_dtype(tensor.dtype)
214
- shape = list(tensor.shape)
215
- num_elements = math.prod(shape) if shape else 1
214
+ tensor_shape = list(tensor.shape)
215
+ shape = torch_shape_to_st_shape(tensor.shape, st_dtype)
216
+ num_elements = math.prod(tensor_shape) if tensor_shape else 1
216
217
  byte_size = num_elements * get_dtype_size(st_dtype)
217
218
  prepared.append((name, st_dtype, shape, byte_size, tensor))
218
219
 
@@ -270,9 +271,10 @@ class IncrementalSafetensorsWriter:
270
271
 
271
272
  # Extract metadata only — no data movement in calling thread
272
273
  st_dtype = torch_to_st_dtype(tensor.dtype)
273
- shape = list(tensor.shape)
274
+ tensor_shape = list(tensor.shape)
275
+ shape = torch_shape_to_st_shape(tensor.shape, st_dtype)
274
276
 
275
- num_elements = math.prod(shape) if shape else 1
277
+ num_elements = math.prod(tensor_shape) if tensor_shape else 1
276
278
  byte_size = num_elements * get_dtype_size(st_dtype)
277
279
 
278
280
  # Record manifest entry
@@ -293,4 +295,4 @@ class IncrementalSafetensorsWriter:
293
295
 
294
296
  self._semaphore.acquire()
295
297
  future = self._executor.submit(self._batch_worker, [(absolute_offset, tensor)])
296
- self._futures.append(future)
298
+ self._futures.append(future)
@@ -11,7 +11,7 @@ import struct
11
11
  from typing import Dict, Optional, Tuple
12
12
 
13
13
  from . import logging_utils
14
- from .tensor_utils import st_to_torch_dtype
14
+ from .tensor_utils import st_shape_to_torch_shape, st_to_torch_dtype
15
15
 
16
16
  logger = logging_utils.get_logger(__name__)
17
17
 
@@ -212,7 +212,8 @@ class UnifiedSafetensorsLoader:
212
212
  if self.low_memory:
213
213
  if key not in self._header:
214
214
  raise KeyError(f"Tensor '{key}' not found in file")
215
- return tuple(self._header[key]["shape"])
215
+ metadata = self._header[key]
216
+ return st_shape_to_torch_shape(metadata["shape"], metadata["dtype"])
216
217
  else:
217
218
  return tuple(self._tensors[key].shape)
218
219
 
@@ -265,9 +266,8 @@ class UnifiedSafetensorsLoader:
265
266
  "ignore", message="The given buffer is not writable"
266
267
  )
267
268
  dtype = st_to_torch_dtype(metadata["dtype"])
268
- tensor = torch.frombuffer(tensor_view, dtype=dtype).view(
269
- metadata["shape"]
270
- )
269
+ shape = st_shape_to_torch_shape(metadata["shape"], metadata["dtype"])
270
+ tensor = torch.frombuffer(tensor_view, dtype=dtype).view(shape)
271
271
  storage = tensor.untyped_storage()
272
272
  setattr(storage, "_uel_mmap_ref", self._mmap)
273
273
  return tensor
@@ -311,7 +311,7 @@ class UnifiedSafetensorsLoader:
311
311
  """Deserialize raw bytes into a torch tensor."""
312
312
  torch = _ensure_torch()
313
313
  dtype_str = metadata["dtype"]
314
- shape = metadata["shape"]
314
+ shape = st_shape_to_torch_shape(metadata["shape"], dtype_str)
315
315
  dtype = st_to_torch_dtype(dtype_str)
316
316
 
317
317
  if tensor_bytes is None:
@@ -76,6 +76,10 @@ def torch_to_st_dtype(dtype) -> str:
76
76
  mapping[torch.float8_e5m2] = "F8_E5M2"
77
77
  if hasattr(torch, "float8_e4m3fn"):
78
78
  mapping[torch.float8_e4m3fn] = "F8_E4M3"
79
+ if hasattr(torch, "float8_e8m0fnu"):
80
+ mapping[torch.float8_e8m0fnu] = "F8_E8M0"
81
+ if hasattr(torch, "float4_e2m1fn_x2"):
82
+ mapping[torch.float4_e2m1fn_x2] = "F4"
79
83
  if hasattr(torch, "uint64"):
80
84
  mapping[torch.uint64] = "U64"
81
85
  if hasattr(torch, "uint32"):
@@ -104,6 +108,8 @@ def st_to_torch_dtype(dtype_str: str):
104
108
  "C64": torch.complex64,
105
109
  "F8_E5M2": getattr(torch, "float8_e5m2", None),
106
110
  "F8_E4M3": getattr(torch, "float8_e4m3fn", None),
111
+ "F8_E8M0": getattr(torch, "float8_e8m0fnu", None),
112
+ "F4": getattr(torch, "float4_e2m1fn_x2", None),
107
113
  "U64": getattr(torch, "uint64", None),
108
114
  "U32": getattr(torch, "uint32", None),
109
115
  "U16": getattr(torch, "uint16", None),
@@ -121,6 +127,28 @@ def get_dtype_size(st_dtype: str) -> int:
121
127
  "I64": 8, "I32": 4, "I16": 2, "I8": 1, "U8": 1,
122
128
  "U64": 8, "U32": 4, "U16": 2,
123
129
  "BOOL": 1, "C64": 8,
124
- "F8_E5M2": 1, "F8_E4M3": 1,
130
+ "F8_E5M2": 1, "F8_E4M3": 1, "F8_E8M0": 1, "F4": 1,
125
131
  }
126
132
  return sizes[st_dtype]
133
+
134
+
135
+ def torch_shape_to_st_shape(shape, st_dtype: str) -> list:
136
+ """Convert a Torch tensor shape to its safetensors header shape."""
137
+ shape = list(shape)
138
+ if st_dtype != "F4":
139
+ return shape
140
+ if not shape:
141
+ raise ValueError("Packed F4 tensors must have at least one dimension")
142
+ shape[-1] *= 2
143
+ return shape
144
+
145
+
146
+ def st_shape_to_torch_shape(shape, st_dtype: str) -> tuple:
147
+ """Convert a safetensors header shape to its Torch tensor shape."""
148
+ shape = list(shape)
149
+ if st_dtype != "F4":
150
+ return tuple(shape)
151
+ if not shape or shape[-1] % 2:
152
+ raise ValueError(f"Invalid packed F4 safetensors shape: {shape}")
153
+ shape[-1] //= 2
154
+ return tuple(shape)
@@ -23,6 +23,9 @@ def _setup_argtypes(lib):
23
23
  lib.model_mmap_get.argtypes = [ctypes.c_void_p]
24
24
  lib.model_mmap_get.restype = ctypes.c_void_p
25
25
 
26
+ lib.model_mmap_get_file_handle.argtypes = [ctypes.c_void_p]
27
+ lib.model_mmap_get_file_handle.restype = ctypes.c_uint64
28
+
26
29
  lib.model_mmap_bounce.argtypes = [ctypes.c_void_p]
27
30
  lib.model_mmap_bounce.restype = ctypes.c_bool
28
31
 
@@ -53,6 +56,9 @@ class ModelMMAP:
53
56
  def get(self):
54
57
  return _ctrl.lib.model_mmap_get(self.state)
55
58
 
59
+ def get_file_handle(self):
60
+ return int(_ctrl.lib.model_mmap_get_file_handle(self.state))
61
+
56
62
  def bounce(self):
57
63
  return bool(_ctrl.lib.model_mmap_bounce(self.state))
58
64
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifiedefficientloader
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: A unified interface for memory efficient per tensor loading of safetensors files as raw bytes from offset, handling CPU/GPU pinned transfers, and converting between tensors and dicts.
5
5
  Author: silveroxides
6
6
  License: MIT License
@@ -33,7 +33,7 @@ Classifier: Operating System :: POSIX :: Linux
33
33
  Requires-Python: >=3.9
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
- Requires-Dist: comfy-aimdo>=0.4.5
36
+ Requires-Dist: comfy-aimdo>=0.5.1
37
37
  Provides-Extra: torch
38
38
  Requires-Dist: torch; extra == "torch"
39
39
  Provides-Extra: safetensors
@@ -5,6 +5,7 @@ setup.py
5
5
  tests/test_direct_gpu.py
6
6
  tests/test_incremental_writer.py
7
7
  tests/test_logging.py
8
+ tests/test_low_bit_dtypes.py
8
9
  tests/test_mmap.py
9
10
  tests/test_mmap_diagnostics.py
10
11
  tests/test_unified_data_loader.py
@@ -1,4 +1,4 @@
1
- comfy-aimdo>=0.4.5
1
+ comfy-aimdo>=0.5.1
2
2
 
3
3
  [all]
4
4
  torch