[diffusion] optimization: pin layerwise host stores in place at their exact size (#39021)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+109
-10
@@ -1,6 +1,7 @@
|
|||||||
import bisect
|
import bisect
|
||||||
import ctypes
|
import ctypes
|
||||||
import ctypes.util
|
import ctypes.util
|
||||||
|
import math
|
||||||
import mmap
|
import mmap
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
@@ -8,6 +9,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import weakref
|
||||||
from collections.abc import Iterator, Mapping, Sequence
|
from collections.abc import Iterator, Mapping, Sequence
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
@@ -395,6 +397,92 @@ _DIRECT_CHUNK = 64 << 20
|
|||||||
MAPPED_DIRECT_READ_MIN_BYTES = 8 * 1024**3
|
MAPPED_DIRECT_READ_MIN_BYTES = 8 * 1024**3
|
||||||
|
|
||||||
|
|
||||||
|
def _pin_in_place(tensor: torch.Tensor) -> bool:
|
||||||
|
"""Page-lock an allocated CPU tensor in place, at exactly its size.
|
||||||
|
|
||||||
|
torch's pinned pool rounds each block up to a power of two (a 1.20 GiB
|
||||||
|
layer store costs 2 GiB), which the pin budget never charged for.
|
||||||
|
"""
|
||||||
|
if not torch.cuda.is_available():
|
||||||
|
return False
|
||||||
|
storage = tensor.untyped_storage()
|
||||||
|
nbytes = storage.nbytes()
|
||||||
|
if nbytes == 0:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
cudart = torch.cuda.cudart()
|
||||||
|
if int(cudart.cudaHostRegister(storage.data_ptr(), nbytes, 0)) != 0:
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
weakref.finalize(storage, _unpin_in_place, storage.data_ptr())
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _unpin_in_place(data_ptr: int) -> None:
|
||||||
|
try:
|
||||||
|
torch.cuda.cudart().cudaHostUnregister(data_ptr)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _shared_storage(nbytes: int) -> Optional[torch.UntypedStorage]:
|
||||||
|
"""Anonymous shared memory (memfd) of exactly `nbytes`, or None off Linux.
|
||||||
|
|
||||||
|
Shared rather than private anonymous memory so the locked pages stay on
|
||||||
|
the shmem side of the process's accounting, where cudaHostAlloc kept
|
||||||
|
them: the host-anon figures (and the forced-host-view budget that
|
||||||
|
subtracts them) keep meaning "pageable copies".
|
||||||
|
"""
|
||||||
|
if not hasattr(os, "memfd_create"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
fd = os.memfd_create("sglang-pinned-store", 0)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
os.ftruncate(fd, nbytes)
|
||||||
|
return torch.UntypedStorage.from_file(
|
||||||
|
f"/proc/self/fd/{fd}", shared=True, nbytes=nbytes
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
|
||||||
|
# Below this, torch's own pinned pool: its power-of-two rounding costs little on
|
||||||
|
# small blocks, and two small blocks can share a page, which cudaHostRegister
|
||||||
|
# refuses to lock twice.
|
||||||
|
_REGISTER_MIN_BYTES = 64 << 20
|
||||||
|
|
||||||
|
|
||||||
|
def _pinned_empty(*size: int, dtype: torch.dtype, stride=None) -> torch.Tensor:
|
||||||
|
"""A pinned CPU tensor of exactly this size; torch's pinned pool as fallback."""
|
||||||
|
shape = size[0] if stride is not None else size
|
||||||
|
if stride is None:
|
||||||
|
storage_numel = math.prod(shape)
|
||||||
|
else:
|
||||||
|
storage_numel = 1 + sum((d - 1) * st for d, st in zip(shape, stride) if d > 0)
|
||||||
|
storage_numel = storage_numel if math.prod(shape) else 0
|
||||||
|
nbytes = storage_numel * dtype.itemsize
|
||||||
|
if nbytes >= _REGISTER_MIN_BYTES:
|
||||||
|
storage = _shared_storage(nbytes)
|
||||||
|
tensor = torch.empty(0, dtype=dtype)
|
||||||
|
if storage is not None:
|
||||||
|
tensor.set_(storage, 0, shape, stride or tensor.stride())
|
||||||
|
elif stride is None:
|
||||||
|
tensor = torch.empty(*size, dtype=dtype)
|
||||||
|
else:
|
||||||
|
tensor = torch.empty_strided(size=shape, stride=stride, dtype=dtype)
|
||||||
|
if _pin_in_place(tensor):
|
||||||
|
return tensor
|
||||||
|
del tensor, storage
|
||||||
|
if stride is None:
|
||||||
|
return torch.empty(*size, dtype=dtype, pin_memory=True)
|
||||||
|
return torch.empty_strided(size=shape, stride=stride, dtype=dtype, pin_memory=True)
|
||||||
|
|
||||||
|
|
||||||
class _DirectReader:
|
class _DirectReader:
|
||||||
"""Read a mapped tensor's bytes from its checkpoint file with O_DIRECT.
|
"""Read a mapped tensor's bytes from its checkpoint file with O_DIRECT.
|
||||||
|
|
||||||
@@ -646,7 +734,11 @@ class MappedLayerCourier:
|
|||||||
[]
|
[]
|
||||||
if direct_copy
|
if direct_copy
|
||||||
else [
|
else [
|
||||||
torch.empty(slot_bytes, dtype=torch.uint8, pin_memory=pin_slots)
|
(
|
||||||
|
_pinned_empty(slot_bytes, dtype=torch.uint8)
|
||||||
|
if pin_slots
|
||||||
|
else torch.empty(slot_bytes, dtype=torch.uint8)
|
||||||
|
)
|
||||||
for _ in range(self._NUM_SLOTS)
|
for _ in range(self._NUM_SLOTS)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -1304,12 +1396,18 @@ class LayerwiseOffloadManager:
|
|||||||
|
|
||||||
# Preserve non-contiguous layouts such as the transposed FP8
|
# Preserve non-contiguous layouts such as the transposed FP8
|
||||||
# weight views expected by CUTLASS kernels.
|
# weight views expected by CUTLASS kernels.
|
||||||
cpu_tensor = torch.empty_strided(
|
if pin_this_layer:
|
||||||
size=local_weight.shape,
|
cpu_tensor = _pinned_empty(
|
||||||
stride=local_weight.stride(),
|
local_weight.shape,
|
||||||
dtype=dtype,
|
dtype=dtype,
|
||||||
pin_memory=pin_this_layer,
|
stride=local_weight.stride(),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
cpu_tensor = torch.empty_strided(
|
||||||
|
size=local_weight.shape,
|
||||||
|
stride=local_weight.stride(),
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
if pin_this_layer:
|
if pin_this_layer:
|
||||||
yield cpu_tensor.untyped_storage()
|
yield cpu_tensor.untyped_storage()
|
||||||
cpu_tensor.copy_(local_weight)
|
cpu_tensor.copy_(local_weight)
|
||||||
@@ -1341,9 +1439,10 @@ class LayerwiseOffloadManager:
|
|||||||
total_numel = current_offset
|
total_numel = current_offset
|
||||||
|
|
||||||
# create concatenated CPU buffer (in pinned memory)
|
# create concatenated CPU buffer (in pinned memory)
|
||||||
cpu_buffer = torch.empty(
|
if pin_this_layer:
|
||||||
total_numel, dtype=dtype, pin_memory=pin_this_layer
|
cpu_buffer = _pinned_empty(total_numel, dtype=dtype)
|
||||||
)
|
else:
|
||||||
|
cpu_buffer = torch.empty(total_numel, dtype=dtype)
|
||||||
if pin_this_layer:
|
if pin_this_layer:
|
||||||
yield cpu_buffer.untyped_storage()
|
yield cpu_buffer.untyped_storage()
|
||||||
|
|
||||||
|
|||||||
@@ -2266,3 +2266,40 @@ def test_mixed_scm_and_dbcache_step_schedule(monkeypatch, step_kinds):
|
|||||||
on_gpu = idx in manager._gpu_layers
|
on_gpu = idx in manager._gpu_layers
|
||||||
assert _layer_weight_ok(model.blocks[idx]) is on_gpu, (kind, idx)
|
assert _layer_weight_ok(model.blocks[idx]) is on_gpu, (kind, idx)
|
||||||
manager.prepare_for_next_req(non_blocking=False)
|
manager.prepare_for_next_req(non_blocking=False)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||||
|
def test_large_pinned_stores_are_registered_in_place_at_exact_size(monkeypatch):
|
||||||
|
pooled = []
|
||||||
|
empty = torch.empty
|
||||||
|
|
||||||
|
def record_pool_use(*args, **kwargs):
|
||||||
|
if kwargs.get("pin_memory"):
|
||||||
|
pooled.append(kwargs)
|
||||||
|
return empty(*args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(torch, "empty", record_pool_use)
|
||||||
|
nbytes = layerwise_offload_mod._REGISTER_MIN_BYTES + 4096
|
||||||
|
tensor = layerwise_offload_mod._pinned_empty(nbytes, dtype=torch.uint8)
|
||||||
|
# locked where it was allocated: pinned, no pool block, no rounding
|
||||||
|
assert tensor.is_pinned()
|
||||||
|
assert tensor.untyped_storage().nbytes() == nbytes
|
||||||
|
assert pooled == []
|
||||||
|
del tensor
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
|
||||||
|
def test_small_pinned_stores_keep_using_the_pool(monkeypatch):
|
||||||
|
pooled = []
|
||||||
|
empty = torch.empty
|
||||||
|
|
||||||
|
def record_pool_use(*args, **kwargs):
|
||||||
|
if kwargs.get("pin_memory"):
|
||||||
|
pooled.append(kwargs)
|
||||||
|
return empty(*args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(torch, "empty", record_pool_use)
|
||||||
|
tensor = layerwise_offload_mod._pinned_empty(1024, dtype=torch.float32)
|
||||||
|
assert tensor.is_pinned()
|
||||||
|
assert len(pooled) == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user