Source code for gigl.distributed.utils.channel
import ctypes
import ctypes.util
import os
import weakref
from functools import cached_property
from itertools import count
from typing import Any, Optional
from graphlearn_torch.channel import SampleMessage, ShmChannel
from gigl.common.metrics.metrics_interface import OpsMetricPublisher
from gigl.src.common.utils.metrics_service_provider import get_metrics_service_instance
[docs]
class SizedShmChannel(ShmChannel):
"""Extends ShmChannel with queue size method `qsize()` by attaching to the channels memory region and inspecting the C++ struct layout.
TODO: Revisit direct memory inspection vs. custom C++ channel implementation. Current solution
inspects GLT's underlying C++ memory layout for simplicity, avoiding the overhead of porting GLT's
full C++ queue code into GiGL. Revisit and implement a native C++ channel with public size methods if:
(a) Deeper channel monitoring is needed (e.g., % queue filled in bytes).
(b) We roll a custom IPC queue for other architectural reasons.
(c) An upstream GLT release breaks the struct memory layout (tests should catch this).
"""
# Class-level handle cached across all instances in the current process
_libc: Optional[ctypes.CDLL] = None
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._finalizer: Optional[weakref.finalize] = None
def __len__(self) -> int:
"""The number of `SampleMessage` items currently in the channel."""
return self.qsize()
[docs]
def qsize(self) -> int:
"""The number of `SampleMessage` items currently in the channel."""
# ShmQueueMeta Memory Layout in Shared Memory (64-bit Architecture)
# Reference: https://github.com/alibaba/graphlearn-for-pytorch/blob/88ff111ac0d9e45c6c9d2d18cfc5883dca07e9f9/graphlearn_torch/include/shm_queue.h#L65
# ==================================================================================
# Byte Offset | C++ Member Variable | Type | Size | Description
# ------------+---------------------+----------+---------+--------------------------
# 00 - 07 | max_block_num_ | size_t | 8 bytes | Capacity (max block count)
# 08 - 15 | max_buf_size_ | size_t | 8 bytes | Max buffer size in bytes
# 16 - 23 | block_meta_offset_ | size_t | 8 bytes | Offset to BlockMeta array
# 24 - 31 | data_buf_offset_ | size_t | 8 bytes | Offset to raw data buffer
# 32 - 39 | write_block_id_ | size_t | 8 bytes | Total messages enqueued <-- READ HERE
# 40 - 47 | read_block_id_ | size_t | 8 bytes | Total messages dequeued <-- READ HERE
# 48 - 55 | alloc_offset_ | size_t | 8 bytes | Internal ring write ptr
# 56 - ... | released_offset_ | size_t | 8 bytes | Internal ring release ptr
# ==================================================================================
ptr = self._shm_ptr
write_block_id = ctypes.c_size_t.from_address(ptr + 32).value
read_block_id = ctypes.c_size_t.from_address(ptr + 40).value
return write_block_id - read_block_id
@cached_property
def _shm_ptr(self) -> int:
# Obtain the shmid from the underlying C++ SampleQueue instance
shmid = self._queue.__getstate__()
# Attach to the shared memory segment in the current process
libc = self._get_libc()
ptr = libc.shmat(shmid, None, 0) # shmat returns (void *)(-1) on failure
if ptr == ctypes.c_void_p(-1).value or ptr is None:
err_num = ctypes.get_errno()
error_msg = os.strerror(err_num)
raise RuntimeError(f"shmat failed for shmid={shmid}: {error_msg}")
# Register automatic cleanup when this object is GC'd in this process
self._finalizer = weakref.finalize(self, libc.shmdt, ptr)
return ptr
@classmethod
def _get_libc(cls) -> ctypes.CDLL:
if cls._libc is None:
libc_path = ctypes.util.find_library("c")
if libc_path is None:
raise RuntimeError(
"Failed to locate standard C library ('libc') via ctypes.util.find_library('c')."
)
libc = ctypes.CDLL(libc_path, use_errno=True)
# void *shmat(int shmid, const void *shmaddr, int shmflg);
libc.shmat.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int]
libc.shmat.restype = ctypes.c_void_p
# int shmdt(const void *shmaddr);
libc.shmdt.argtypes = [ctypes.c_void_p]
libc.shmdt.restype = ctypes.c_int
cls._libc = libc
return cls._libc
def __getstate__(self) -> dict[str, Any]:
# Invalidate cached pointer and finalizer across process boundaries
state = self.__dict__.copy()
state.pop("_shm_ptr", None)
state.pop("_finalizer", None)
return state
[docs]
class MonitoredShmChannel(SizedShmChannel):
# Counts instantiations of this class, per process.
# This is needed so we can generate unique channel names for each instance within the same process.
# NOTE: This is per-class, not per-instance.
_counter = count(0)
def __init__(self, channel_name: str, *args, **kwargs) -> None:
"""Monitored variant of SizedShmChannel that integrates with GiGL metrics service and records queue size on recv() as a gauge.
Args:
channel_name: Prefix for published metrics. Must be unique across
processes to disambiguate channels owned by different dataloaders.
Multiple instances within the same process are automatically
disambiguated by an internally appended sequence ID (e.g., `id0`).
*args: Positional arguments forwarded directly to `ShmChannel`.
**kwargs: Keyword arguments forwarded directly to `ShmChannel`.
Example:
Passing `channel_name="my_channel_pid_12345"` publishes queue size for
the first instance as `my_channel_pid_12345_id0_qsize`.
"""
super().__init__(*args, **kwargs)
self._channel_name = f"{channel_name}_id{next(self._counter)}"
[docs]
def recv(self, *args, **kwargs) -> SampleMessage:
publisher: Optional[OpsMetricPublisher] = get_metrics_service_instance()
if publisher is None:
raise RuntimeError(
"Failed to record channel metrics in MonitoredShmChannel: the metric publisher "
"could not be retrieved. Check logs for metrics class construction errors."
)
publisher.add_gauge(f"{self._channel_name}_qsize", self.qsize())
return super().recv(*args, **kwargs)