gigl.utils.share_memory#
Attributes#
Functions#
|
An EMPTY writable tensor whose bytes live in a file, or None to fall back to RAM. |
|
Allocate a large tensor in its FINAL home, so |
|
Whether THIS process still maps |
|
Whether |
Whether |
|
|
Re-map a spilled tensor in the current process, without copying its bytes anywhere. |
Clear leftover spill files, ONCE per run, before anything in the run can spill. |
|
|
Write a disk-backed tensor's dirty pages out and ask the kernel to drop them from the cache. |
Request eviction of a spill file's page cache when nothing maps it any more. |
|
|
Based on GraphLearn-for-PyTorch's share_memory implementation, with additional support for handling empty tensors with share_memory. |
|
Prepare a mapping of tensors to cross a process boundary, spilling instead of copying. |
|
Write |
Module Contents#
- gigl.utils.share_memory.allocate_disk_backed(shape, dtype)[source]#
An EMPTY writable tensor whose bytes live in a file, or None to fall back to RAM.
The counterpart to
spill_tensor_to_disk()for a buffer that does not exist yet. Spilling fills RAM and then copies it out, so it needs the full size in anonymous memory at least once; allocating the destination as the file from the start means those bytes never occupy anonymous memory at all.Blocks are RESERVED up front with
posix_fallocaterather than left sparse, and reservation is mandatory.from_filegives the file its full apparent size without allocating it, so a filesystem that fills later fails at the moment a page is written – and a write to a mapping that cannot be backed raises SIGBUS, which is a signal, not an exception: it bypasses everytryhere and kills the process with no traceback. Reserving turns that into anOSErrorat allocation time, where it can be reported and fallen back on; a filesystem that cannot reserve at all gets None rather than an unreserved mapping.Sequential writes through the mapping cost ~3.1x their in-memory equivalent (measured on a wide fp32 matrix written row-by-row). What that buys: the destination’s bytes stop being unreclaimable anonymous memory – still charged to the cgroup while resident, but reclaimable page cache the kernel can evict under pressure instead of OOM-killing.
Returns None whenever a file-backed buffer is not available or not worth it – spilling disabled, below the size threshold, quantized dtype, no room to reserve, reservation unsupported by the filesystem, IO failure – so callers can simply fall back to
torch.empty.- Parameters:
shape (tuple[int, Ellipsis])
dtype (torch.dtype)
- Return type:
Optional[torch.Tensor]
- gigl.utils.share_memory.allocate_preshared(shape, dtype, random_access=False)[source]#
Allocate a large tensor in its FINAL home, so
share_memory_()cannot duplicate it.GLT’s
Graph.__init__shares the topology unconditionally, and for an anonymous tensor that copies every byte into/dev/shm– the tensor exists twice during the copy, fatal for a tens-of-GiB CSR near the memory limit. Both homes here are immune to that copy: a spill file (consumers that checkis_disk_backed()leave it alone), or POSIX shared memory allocated directly (share_memory_()then finds it already shared and does nothing).Disk-first for a destination written in order. A scatter destination prefers memory: an 8-byte write to an uncached file page is a 4 KiB read-modify-write, repeated over the same pages, so a file-backed scatter destination behaves like a hung run. With
random_access=Truea file is only the checked, loudly-logged fallback for when memory will not hold the tensor.Below the spill threshold returns a plain tensor. Uninitialised in all cases, like
torch.empty.- Parameters:
shape (tuple[int, Ellipsis]) – Shape of the tensor.
dtype (torch.dtype) – Dtype of the tensor.
random_access (bool) – Set when the caller will write to scattered offsets rather than stream through in order; memory is then preferred and disk the checked fallback.
- Return type:
torch.Tensor
- gigl.utils.share_memory.has_live_mapping(path)[source]#
Whether THIS process still maps
path, read from/proc/self/maps.The kernel’s own list of this process’s mappings, so it sees every mapping over the file no matter who created it. False when the list cannot be read (non-Linux), matching the rest of this module: spilling is effectively Linux-only and refuses gracefully elsewhere.
- Parameters:
path (str)
- Return type:
bool
- gigl.utils.share_memory.is_disk_backed(tensor)[source]#
Whether
tensor(or a view of one) is an mmap over a real file rather than RAM.Read off the tensor itself: a storage created by
from_file(shared=True)records the path inuntyped_storage().filename, and a view shares its base’s storage, so the label travels with it.filenameis None for everything else – including tmpfs-shared storages (share_memory_(),_new_shared), which are RAM and must not be treated as spilled.- Parameters:
tensor (torch.Tensor)
- Return type:
bool
- gigl.utils.share_memory.is_tensor_spilling_enabled()[source]#
Whether
GIGL_TENSOR_SPILL_DIRis set, i.e. large tensors should go to disk.- Return type:
bool
- gigl.utils.share_memory.load_spilled_tensor(path, dtype, shape)[source]#
Re-map a spilled tensor in the current process, without copying its bytes anywhere.
The counterpart to
spill_tensor_to_disk()for a path that arrived through some channel other than pickling a tensor (pickling already re-maps by itself).- Raises:
ValueError – If the file is the wrong size for
shape– which would otherwise be read as silently wrong data.OSError – If the file cannot be mapped writable.
- Parameters:
path (str)
dtype (torch.dtype)
shape (tuple[int, Ellipsis])
- Return type:
torch.Tensor
- gigl.utils.share_memory.prepare_spill_dir()[source]#
Clear leftover spill files, ONCE per run, before anything in the run can spill.
Cleanup is done at the START of a run, not with
atexit. The tensors are spilled inside themp.spawndataset-building child (dataset_factory._build_dataset_process), and that child exits before the trainer uses the dataset – so unlinking on its exit would delete files the trainer and its sampling workers still have mapped by path. Removing stale files up front bounds disk use without that hazard.Exactly one process may do this, and it must be a process that starts before any spilling one – a run has several spilling children in sequence, and a later one cannot tell a sibling’s live files from a stale run’s (the first spiller does clean up itself when no ancestor did, which covers standalone use and tests). Per-process age-based cleanup is NOT a substitute and is actively wrong: with sequential loading, the edge child spills and exits, then the node child starts, sees the edge files as older than itself, and deletes files the parent has not mapped yet. The marker below is an environment variable precisely because
spawnchildren inherit the environment, so a child can tell that its parent already did this.Idempotent, and safe to call when spilling is disabled.
- Return type:
None
- gigl.utils.share_memory.release_page_cache(tensor)[source]#
Write a disk-backed tensor’s dirty pages out and ask the kernel to drop them from the cache.
A spilled tensor’s pages are reclaimable but still charged to cgroup v2’s
memory.currentwhile resident; dropping them releases the charge. The mapping stays valid and refaults on next access, so this is worth it only for data written now and not read until much later. Operates on the tensor’s whole backing file, so views over the same file lose their cached pages too.Three steps, in this order, and every one is load-bearing:
fsync– writes back the pages dirtied through the mapping; dirty pages cannot be dropped at all.MADV_DONTNEEDon the mapping – removes this process’s page-table entries. Without it step 3 silently skips every still-mapped page and reports success having freed nothing.POSIX_FADV_DONTNEED– discards the now-unmapped clean page cache.
Returns True when every step COMPLETED –
FADV_DONTNEEDis advisory, so completion is not proof of eviction; measure residency withmincoreto know (the tests do). False means the sequence was incomplete, though a failure after step 2 has already dropped this process’s page-table entries.- Parameters:
tensor (torch.Tensor)
- Return type:
bool
- gigl.utils.share_memory.release_page_cache_by_path(path)[source]#
Request eviction of a spill file’s page cache when nothing maps it any more.
The counterpart to
release_page_cache()for a tensor already dropped:delunmaps it but leaves the file’s pages charged to the cgroup until the kernel reclaims them.Refuses when a mapping is still live:
FADV_DONTNEEDsilently skips pages present in any page table, so it would return success having freed nothing. Userelease_page_cache()on the tensor in that case, which unmaps first.Returns True if the eviction was REQUESTED (
FADV_DONTNEEDis advisory and reports no count); False if a mapping is still live, the file could not be opened, or the platform lacksposix_fadvise.- Parameters:
path (str)
- Return type:
bool
- gigl.utils.share_memory.share_memory(entity)[source]#
- Based on GraphLearn-for-PyTorch’s share_memory implementation, with additional support for handling empty tensors with share_memory.
Calling share_memory_() on an empty tensor may cause processes to hang, although the root cause of this is currently unknown. As a result, we opt to not move empty tensors to shared memory if they are provided.
When calling share_memory on a RangePartitionBook, we don’t need to move the partition bounds to shared memory, since GLT doesn’t natively provide a ForkingPickler registration method for the RangePartitionBook, and the cost of not moving this to shared memory is minimal, since the size of this tensor is very small, being equal in length to the number of machines.
This function never spills to disk, and a tensor that is ALREADY disk-backed is left alone:
share_memory_()on an mmap-backed tensor copies every byte into/dev/shm, quietly undoing a spill made elsewhere. Spilling belongs toshare_memory_for_ipc()andspill_tensor_to_disk().- Parameters:
entity (Optional[Union[torch.Tensor, PartitionBook, dict[_KeyType, torch.Tensor], dict[_KeyType, PartitionBook]]]) – Homogeneous or heterogeneous entity of tensors which is being moved to shared memory
- Return type:
None
- gigl.utils.share_memory.share_memory_for_ipc(entity)[source]#
Prepare a mapping of tensors to cross a process boundary, spilling instead of copying.
Same intent as
share_memory(), but for values that will be pickled to another process: each value large enough is spilled first, and a spilled tensor pickles as its file path (see_reduce_spill_aware()), so its bytes never transit/dev/shm. Tensors that are not spilled (spilling disabled, below the size threshold) go to POSIX shared memory exactly as before, which pickles by handle already.Returns a NEW dict; the input is left alone, since the caller usually still holds the tensors and dropping them is its decision.
- Parameters:
entity (dict[_KeyType, torch.Tensor])
- Return type:
dict[_KeyType, torch.Tensor]
- gigl.utils.share_memory.spill_tensor_to_disk(tensor)[source]#
Write
tensorto disk and return a tensor mapped over the file, orNoneto keep it.Nonecovers every reason not to spill – spilling disabled, tensor below the threshold, IO failure – so callers can treat it as “keep what you had”. The returned tensor’s storage knows its file (is_disk_backed()), soshare_memory()leaves it alone and pickling ships the path rather than the bytes.Exposed for callers that hold a tensor directly rather than inside a Mapping, which
share_memory()cannot substitute into. The partitioned node feature matrix is the case that matters: it is created after loading, so the spill inload_torch_tensorsnever sees it, and it is resident during the graph build.- Parameters:
tensor (torch.Tensor)
- Return type:
Optional[torch.Tensor]