[diffusion] UX: report where a component's weights are (#35618)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Utilities for selecting and loading models."""
|
||||
|
||||
import bisect
|
||||
import contextlib
|
||||
import glob
|
||||
import json
|
||||
@@ -333,5 +334,125 @@ def get_memory_usage_of_component(module) -> float | None:
|
||||
return round(usage, 2)
|
||||
|
||||
|
||||
def _read_process_mappings() -> tuple[list[int], list[int], list[bool]] | None:
|
||||
"""Sorted (start, end, is_file_backed) of this process's address space.
|
||||
|
||||
Linux only; returns None where /proc is unavailable, and the caller then
|
||||
reports host bytes without splitting file-backed from anonymous.
|
||||
"""
|
||||
try:
|
||||
with open("/proc/self/maps") as handle:
|
||||
rows = []
|
||||
for line in handle:
|
||||
fields = line.split(maxsplit=5)
|
||||
low, _, high = fields[0].partition("-")
|
||||
path = fields[5].strip() if len(fields) > 5 else ""
|
||||
# pseudo-paths like [heap] and [stack] are anonymous
|
||||
rows.append(
|
||||
(int(low, 16), int(high, 16), bool(path) and path[0] != "[")
|
||||
)
|
||||
except OSError:
|
||||
return None
|
||||
rows.sort()
|
||||
return [r[0] for r in rows], [r[1] for r in rows], [r[2] for r in rows]
|
||||
|
||||
|
||||
def component_residency_bytes(module) -> Dict[str, int]:
|
||||
"""Where a component's weights actually sit, in bytes.
|
||||
|
||||
Four buckets, ordered by what the kernel can do with them: device memory,
|
||||
pinned host memory (which it cannot reclaim at all), file-backed host
|
||||
memory (which it can drop without swapping), and anonymous host memory.
|
||||
|
||||
Two caveats. `host_mapped` counts the size of the file mapping, not the
|
||||
pages currently resident in it -- a mapped safetensors file is faulted in
|
||||
lazily, so the real footprint is at most this. And pinned is tested first
|
||||
because CUDA's host allocator sits behind a named mapping, which the
|
||||
file-backed check alone would misread.
|
||||
|
||||
Layerwise-offloaded weights are absent from parameters()/buffers(): the
|
||||
module keeps (1,) placeholders while its offload managers own the host
|
||||
copy, so those managers are walked too. Sizes are taken from the storage
|
||||
and deduped by it, because one flat host buffer backs many logical weights.
|
||||
"""
|
||||
if not isinstance(module, nn.Module):
|
||||
return {}
|
||||
|
||||
totals = {"vram": 0, "host_pinned": 0, "host_mapped": 0, "host": 0}
|
||||
seen: set[int] = set()
|
||||
mappings = _read_process_mappings()
|
||||
|
||||
def is_file_backed(pointer: int) -> bool:
|
||||
if mappings is None:
|
||||
return False
|
||||
starts, ends, backed = mappings
|
||||
index = bisect.bisect_right(starts, pointer) - 1
|
||||
if index < 0 or pointer >= ends[index]:
|
||||
return False
|
||||
return backed[index]
|
||||
|
||||
def add(tensor: torch.Tensor) -> None:
|
||||
try:
|
||||
storage = tensor.untyped_storage()
|
||||
pointer = storage.data_ptr()
|
||||
except Exception:
|
||||
return
|
||||
# a zero pointer is an empty offload placeholder, not a weight
|
||||
if pointer == 0 or pointer in seen:
|
||||
return
|
||||
seen.add(pointer)
|
||||
if tensor.device.type != "cpu":
|
||||
totals["vram"] += storage.nbytes()
|
||||
return
|
||||
try:
|
||||
pinned = tensor.is_pinned()
|
||||
except Exception:
|
||||
pinned = False
|
||||
if pinned:
|
||||
bucket = "host_pinned"
|
||||
elif is_file_backed(pointer):
|
||||
bucket = "host_mapped"
|
||||
else:
|
||||
bucket = "host"
|
||||
totals[bucket] += storage.nbytes()
|
||||
|
||||
for tensor in module.parameters():
|
||||
add(tensor)
|
||||
for tensor in module.buffers():
|
||||
add(tensor)
|
||||
for manager in getattr(module, "layerwise_offload_managers", None) or []:
|
||||
iter_cpu_weights = getattr(manager, "iter_cpu_weights", None)
|
||||
if iter_cpu_weights is None:
|
||||
continue
|
||||
for _, tensor in iter_cpu_weights():
|
||||
add(tensor)
|
||||
|
||||
return totals
|
||||
|
||||
|
||||
def format_component_residency(module) -> str:
|
||||
"""Name the places a component's weights are, skipping the empty ones.
|
||||
|
||||
A component that streams from the host reports no VRAM at rest, which is
|
||||
the point; saying so beats reporting a zero delta that reads as free.
|
||||
"""
|
||||
totals = component_residency_bytes(module)
|
||||
# `pinned` and `pageable` are the standard CUDA pair, and naming mmap after
|
||||
# the call says what it is: labels a reader has to guess at defeat the point
|
||||
# of splitting host bytes in the first place.
|
||||
labels = (
|
||||
("vram", "vram"),
|
||||
("host_pinned", "host pinned"),
|
||||
("host_mapped", "host mmap"),
|
||||
("host", "host pageable"),
|
||||
)
|
||||
parts = [
|
||||
f"{label}: {totals[key] / BYTES_PER_GB:.2f} GB"
|
||||
for key, label in labels
|
||||
if totals.get(key)
|
||||
]
|
||||
return ", ".join(parts) if parts else "weights: none"
|
||||
|
||||
|
||||
# component name -> ComponentLoader class
|
||||
component_name_to_loader_cls: Dict[str, Type[Any]] = {}
|
||||
|
||||
Reference in New Issue
Block a user