Skip to content

Commit bd8b2be

Browse files
authored
Avoid rereading CUDA weights blob for SHA-256 computation (pytorch#21800)
## Summary - Compute SHA-256 while streaming the CUDA AOTI external weights blob to disk, avoiding a second full read of the approximately 19 GB weights file during serialization. - Preserve lazy SHA-256 computation for callers that do not provide a digest. ## Performance In an independent MG 30B cold export with an empty Inductor cache, export time improved from **21:16.45 to 18:56.75**, a **10.9% reduction**.
1 parent 2c88f57 commit bd8b2be

3 files changed

Lines changed: 46 additions & 12 deletions

File tree

‎backends/cuda/cuda_backend.py‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ctypes
1111
import functools
1212
import gc
13+
import hashlib
1314
import logging
1415
import os
1516
import shutil
@@ -401,12 +402,17 @@ def _on_off_compile_spec_value(spec: CompileSpec) -> bool:
401402
return value == "ON"
402403

403404

404-
def _write_aoti_weights_blob(weights, blob_path: str) -> None:
405-
"""Stream AOTI tensor storages without creating a model-sized bytes object."""
405+
def _write_aoti_weights_blob(weights, blob_path: str) -> bytes:
406+
"""Stream AOTI tensor storages to disk and return their SHA-256 digest."""
406407
_trim_host_memory()
407408
tensors = [tensor for tensor, _ in weights.values()]
408409
all_cuda = all(tensor.is_cuda for tensor in tensors)
409410
chunk_size = 8 * 1024 * 1024
411+
digest = hashlib.sha256()
412+
413+
def write_chunk(output, chunk) -> None:
414+
digest.update(chunk)
415+
output.write(chunk)
410416

411417
with open(blob_path, "wb") as output:
412418
for tensor in tensors:
@@ -420,20 +426,21 @@ def _write_aoti_weights_blob(weights, blob_path: str) -> None:
420426
).set_(storage, 0, (nbytes,), (1,))
421427
for offset in range(0, nbytes, chunk_size):
422428
cpu_chunk = byte_tensor[offset : offset + chunk_size].cpu()
423-
output.write(memoryview(cpu_chunk.numpy()))
429+
write_chunk(output, memoryview(cpu_chunk.numpy()))
424430
del byte_tensor, cpu_chunk
425431
elif nbytes:
426432
raw_array = (ctypes.c_ubyte * nbytes).from_address(storage.data_ptr())
427433
raw_view = memoryview(raw_array).cast("B")
428434
for offset in range(0, nbytes, chunk_size):
429-
output.write(raw_view[offset : offset + chunk_size])
435+
write_chunk(output, raw_view[offset : offset + chunk_size])
430436
del raw_view, raw_array
431437
# Match AOTInductor's binary_blob layout: CUDA-only constants are
432438
# packed, while CPU/mixed constants are aligned to 64 bytes.
433439
if not all_cuda and (padding := (-nbytes) % 64):
434-
output.write(bytes(padding))
440+
write_chunk(output, bytes(padding))
435441
del storage
436442
_trim_host_memory()
443+
return digest.digest()
437444

438445

439446
@final
@@ -447,6 +454,11 @@ class CudaBackend(AotiBackend, BackendDetails):
447454
using the Executorch runtime.
448455
"""
449456

457+
# AOTI calls materialize_weights_blob immediately before load_weights_blob
458+
# for a given output path. A new materialization overwrites any digest left
459+
# behind by an export that aborted before the consumer ran.
460+
_materialized_blob_hashes: Dict[str, bytes] = {}
461+
450462
@classmethod
451463
def get_device_name(cls) -> str:
452464
return "cuda"
@@ -562,8 +574,10 @@ def load_weights_blob(
562574
"""
563575
if not cls._is_low_memory_mode(compile_specs):
564576
return super().load_weights_blob(blob_path, compile_specs)
565-
blob_data = FileBackedData.move_from(blob_path)
566-
return blob_data, blob_data.sha256().hex()
577+
known_hash = cls._materialized_blob_hashes.pop(blob_path, None)
578+
blob_data = FileBackedData.move_from(blob_path, sha256=known_hash)
579+
weights_blob_hash = known_hash or blob_data.sha256()
580+
return blob_data, weights_blob_hash.hex()
567581

568582
@classmethod
569583
def materialize_weights_blob(
@@ -593,7 +607,9 @@ def materialize_weights_blob(
593607
if so_path is None:
594608
raise RuntimeError(f"Expected a CUDA AOTI .wrapper.so output, got {paths}")
595609
blob_path = os.path.splitext(so_path)[0] + "_weights.blob"
596-
_write_aoti_weights_blob(weights[0], blob_path)
610+
cls._materialized_blob_hashes[blob_path] = _write_aoti_weights_blob(
611+
weights[0], blob_path
612+
)
597613

598614
# Forcing the external-weights ABI makes Inductor emit an empty blob
599615
# path alongside the Weights object. Replace that file in place and do

‎backends/cuda/tests/test_cuda_partitioner.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ def test_low_memory_weights_are_streamed_in_binary_blob_format(self) -> None:
7979
)
8080
self.assertEqual(expected, data)
8181

82+
# The streaming write computes the digest in the same pass. Loading
83+
# the file-backed blob must not reread it solely for hashing.
84+
with patch.object(
85+
FileBackedData,
86+
"sha256",
87+
side_effect=AssertionError("unexpected blob reread"),
88+
):
89+
blob, digest = CudaBackend.load_weights_blob(
90+
blob_path, [CompileSpec("low_memory_mode", b"ON")]
91+
)
92+
self.assertEqual(hashlib.sha256(expected).hexdigest(), digest)
93+
self.assertEqual(expected, blob.to_bytes())
94+
8295
def test_low_memory_weights_require_wrapper_so(self) -> None:
8396
tensor = torch.tensor([1], dtype=torch.int16)
8497
weights = Weights({"weight": (tensor, TensorProperties(tensor))})

‎exir/_serialize/_cord.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@ class FileBackedData:
1818

1919
_COPY_CHUNK_SIZE = 8 * 1024 * 1024
2020

21-
def __init__(self, path: str, cleanup: bool = False) -> None:
21+
def __init__(
22+
self,
23+
path: str,
24+
cleanup: bool = False,
25+
sha256: Optional[bytes] = None,
26+
) -> None:
2227
self._path = path
2328
self._size = os.path.getsize(path)
24-
self._sha256: Optional[bytes] = None
29+
self._sha256 = sha256
2530
self._finalizer = (
2631
weakref.finalize(self, self._remove, path) if cleanup else None
2732
)
@@ -34,7 +39,7 @@ def _remove(path: str) -> None:
3439
pass
3540

3641
@classmethod
37-
def move_from(cls, path: str) -> "FileBackedData":
42+
def move_from(cls, path: str, sha256: Optional[bytes] = None) -> "FileBackedData":
3843
"""Take ownership of ``path`` without loading its contents."""
3944
directory = os.path.dirname(path) or "."
4045
fd, owned_path = tempfile.mkstemp(
@@ -46,7 +51,7 @@ def move_from(cls, path: str) -> "FileBackedData":
4651
except Exception:
4752
os.remove(owned_path)
4853
raise
49-
return cls(owned_path, cleanup=True)
54+
return cls(owned_path, cleanup=True, sha256=sha256)
5055

5156
def __len__(self) -> int:
5257
return self._size

0 commit comments

Comments
 (0)