Source code for gigl.common.data.load_torch_tensors

import time
import traceback
from dataclasses import dataclass, replace
from typing import (
    Generic,
    Mapping,
    MutableMapping,
    NamedTuple,
    Optional,
    TypeVar,
    Union,
    cast,
)

import torch
import torch.multiprocessing as mp
from graphlearn_torch.distributed.rpc import barrier, rpc_is_initialized
from torch.multiprocessing import Manager

from gigl.common.data.dataloaders import (
    SerializedTFRecordInfo,
    TFDatasetOptions,
    TFRecordDataLoader,
)
from gigl.common.logger import Logger
from gigl.common.utils.feature_quantization.lossless_compression import (
    FeatureCompression,
    compress_loaded_features,
    resolve_feature_compression,
)
from gigl.src.common.types.graph_data import EdgeType, NodeType
from gigl.types.graph import (
    DEFAULT_HOMOGENEOUS_EDGE_TYPE,
    DEFAULT_HOMOGENEOUS_NODE_TYPE,
    FeatureQuantizationMetadata,
    LoadedGraphTensors,
)
from gigl.utils.share_memory import share_memory

[docs] logger = Logger()
_ID_FMT = "{entity}_ids" _FEATURE_FMT = "{entity}_features" _PACKED_FEATURE_FMT = "{entity}_packed_features" _LABEL_FMT = "{entity}_labels" _EDGE_WEIGHTS_KEY = "edge_weights" _NODE_KEY = "node" def _extract_weight_col( feat_tensor: torch.Tensor, feature_keys: list[str], feature_spec: dict, col_name: str, edge_type: EdgeType, ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: """Slice a named weight column out of a feature tensor. Accounts for multi-dim features: each feature key may contribute more than one column to ``feat_tensor`` (e.g. ``FixedLenFeature(shape=[16])`` contributes 16 columns). The weight feature must be a scalar (width 1). Args: feat_tensor: Edge feature tensor of shape ``[num_edges, total_feature_cols]``. feature_keys: Ordered list of feature names matching the columns of ``feat_tensor``. feature_spec: Feature spec dict mapping feature name to its TF feature spec (used to determine per-key column widths). col_name: Name of the column to extract as weights. edge_type: Edge type (used only in error messages). Returns: A tuple ``(weights, trimmed_features)`` where ``weights`` is a 1-D tensor of shape ``[num_edges]`` and ``trimmed_features`` is ``feat_tensor`` with the weight column removed. Raises: ValueError: If ``col_name`` is not in ``feature_keys`` or the weight feature is not width 1. """ if col_name not in feature_keys: raise ValueError( f"weight_edge_feat_name '{col_name}' not found in edge feature keys " f"for edge type {edge_type}: {feature_keys}" ) key_idx = feature_keys.index(col_name) col_widths = [] for key in feature_keys: spec = feature_spec[key] col_widths.append(spec.shape[-1] if spec.shape else 1) weight_width = col_widths[key_idx] if weight_width != 1: raise ValueError( f"weight_edge_feat_name '{col_name}' for edge type {edge_type} must be a scalar " f"feature (width 1), but has width {weight_width}." ) col_offset = sum(col_widths[:key_idx]) weights = feat_tensor[:, col_offset] keep_cols = [i for i in range(feat_tensor.shape[1]) if i != col_offset] trimmed = feat_tensor[:, keep_cols] if keep_cols else None return weights, trimmed _EDGE_KEY = "edge" _POSITIVE_LABEL_KEY = "positive_label" _NEGATIVE_LABEL_KEY = "negative_label" @dataclass(frozen=True)
[docs] class SerializedGraphMetadata: """ Stores information for all entities. If homogeneous, all types are of type SerializedTFRecordInfo. Otherwise, they are dictionaries with the corresponding mapping. """ # Node Entity Info for loading node tensors, a SerializedTFRecordInfo for homogeneous and dict[NodeType, SerializedTFRecordInfo] for heterogeneous cases
[docs] node_entity_info: Union[ SerializedTFRecordInfo, dict[NodeType, SerializedTFRecordInfo] ]
# Edge Entity Info for loading edge tensors, a SerializedTFRecordInfo for homogeneous and dict[EdgeType, SerializedTFRecordInfo] for heterogeneous cases
[docs] edge_entity_info: Union[ SerializedTFRecordInfo, dict[EdgeType, SerializedTFRecordInfo] ]
# Positive Label Entity Info, if present, a SerializedTFRecordInfo for homogeneous and dict[EdgeType, SerializedTFRecordInfo] for heterogeneous cases. # If there are no positive labels for any edge type, this value is None
[docs] positive_label_entity_info: Optional[ Union[SerializedTFRecordInfo, dict[EdgeType, SerializedTFRecordInfo]] ] = None
# Negative Label Entity Info, if present, a SerializedTFRecordInfo for homogeneous and dict[EdgeType, SerializedTFRecordInfo] for heterogeneous cases. # If there are no negative labels for any edge type, this value is None.
[docs] negative_label_entity_info: Optional[ Union[SerializedTFRecordInfo, dict[EdgeType, SerializedTFRecordInfo]] ] = None
# Optional node quantization metadata.
[docs] node_quantization_metadata: Optional[ Union[FeatureQuantizationMetadata, dict[NodeType, FeatureQuantizationMetadata]] ] = None
[docs] edge_quantization_metadata: Optional[ Union[FeatureQuantizationMetadata, dict[EdgeType, FeatureQuantizationMetadata]] ] = None
def _validate_weight_edge_feature_name( edge_entity_info: Union[ SerializedTFRecordInfo, dict[EdgeType, SerializedTFRecordInfo] ], weight_edge_feat_name: Optional[Union[str, dict[EdgeType, str]]], ) -> None: if weight_edge_feat_name is None: return configured_weights: list[tuple[EdgeType, str, SerializedTFRecordInfo]] if isinstance(edge_entity_info, SerializedTFRecordInfo): if not isinstance(weight_edge_feat_name, str): raise ValueError("weight_edge_feat_name must be str for homogeneous graph") edge_type = DEFAULT_HOMOGENEOUS_EDGE_TYPE configured_weights = [(edge_type, weight_edge_feat_name, edge_entity_info)] else: if isinstance(weight_edge_feat_name, str): if len(edge_entity_info) != 1: raise ValueError( "weight_edge_feat_name must be dict[EdgeType, str] for heterogeneous graph with multiple edge types" ) edge_type, serialized_info = next(iter(edge_entity_info.items())) configured_weights = [(edge_type, weight_edge_feat_name, serialized_info)] else: unknown_edge_types = set(weight_edge_feat_name) - set(edge_entity_info) if unknown_edge_types: raise ValueError( f"weight_edge_feat_name contains unknown edge types: {unknown_edge_types}" ) configured_weights = [ (edge_type, feature_name, edge_entity_info[edge_type]) for edge_type, feature_name in weight_edge_feat_name.items() ] for edge_type, feature_name, serialized_info in configured_weights: if feature_name not in serialized_info.feature_keys: raise ValueError( f"Sampling-weight field '{feature_name}' for edge type {edge_type} must be an unquantized raw edge feature." ) def _shift_indices_past_removed_column( indices: tuple[int, ...], removed_index: int ) -> tuple[int, ...]: """Shifts every index after ``removed_index`` one position to the left.""" shifted_indices: list[int] = [] for index in indices: if index > removed_index: shifted_indices.append(index - 1) else: shifted_indices.append(index) return tuple(shifted_indices)
[docs] def remove_sampling_weight_from_edge_quantization_metadata( serialized_graph_metadata: SerializedGraphMetadata, weight_edge_feat_name: Optional[Union[str, dict[EdgeType, str]]], ) -> Optional[ Union[FeatureQuantizationMetadata, dict[EdgeType, FeatureQuantizationMetadata]] ]: """Remove separately stored sampling weights from edge reconstruction metadata. TFRecord loading removes the sampling-weight column from raw edge features before registering it with the weighted sampler. The resulting metadata must describe the remaining model features so batch reconstruction scatters raw and dequantized columns into the correct positions. Args: serialized_graph_metadata: Serialized edge schema and quantization metadata. weight_edge_feat_name: Raw scalar feature configured as sampling weights. Returns: Quantization metadata for the model-facing edge features. """ quantization_metadata = serialized_graph_metadata.edge_quantization_metadata if quantization_metadata is None or weight_edge_feat_name is None: return quantization_metadata if isinstance(serialized_graph_metadata.edge_entity_info, SerializedTFRecordInfo): assert isinstance(quantization_metadata, FeatureQuantizationMetadata) assert isinstance(weight_edge_feat_name, str) edge_info_by_type: dict[EdgeType, SerializedTFRecordInfo] = { DEFAULT_HOMOGENEOUS_EDGE_TYPE: serialized_graph_metadata.edge_entity_info } metadata_by_type: dict[EdgeType, FeatureQuantizationMetadata] = { DEFAULT_HOMOGENEOUS_EDGE_TYPE: quantization_metadata } weight_by_type: dict[EdgeType, str] = { DEFAULT_HOMOGENEOUS_EDGE_TYPE: weight_edge_feat_name } is_homogeneous = True else: assert isinstance(quantization_metadata, dict) edge_info_by_type: dict[EdgeType, SerializedTFRecordInfo] = ( serialized_graph_metadata.edge_entity_info ) metadata_by_type: dict[EdgeType, FeatureQuantizationMetadata] = cast( dict[EdgeType, FeatureQuantizationMetadata], quantization_metadata ) if isinstance(weight_edge_feat_name, str): if len(edge_info_by_type) != 1: raise ValueError( "weight_edge_feat_name must be dict[EdgeType, str] for " "heterogeneous graph with multiple edge types" ) edge_type = next(iter(edge_info_by_type)) weight_by_type: dict[EdgeType, str] = {edge_type: weight_edge_feat_name} else: weight_by_type: dict[EdgeType, str] = weight_edge_feat_name is_homogeneous = False adjusted_metadata: dict[EdgeType, FeatureQuantizationMetadata] = {} for edge_type, metadata in metadata_by_type.items(): weight_feature_name = weight_by_type.get(edge_type) if weight_feature_name is None: adjusted_metadata[edge_type] = metadata continue edge_info = edge_info_by_type[edge_type] raw_column_offset = 0 for feature_name in edge_info.feature_keys: if feature_name == weight_feature_name: break feature_spec = edge_info.feature_spec[feature_name] raw_column_offset += feature_spec.shape[-1] if feature_spec.shape else 1 # The sampling weight must stay an uncompressed raw column, and the loader removes it from the edge # features. Every logical column after it moves one position to the left, so the packed column indices # must move with it. Lossless indices are logical column indices too, so they shift like the quantized # ones; otherwise a lossless column after the weight column decodes into the wrong position. # # Example, with 5 logical columns: # before: 0 age (raw), 1 weight (raw), 2 country (quantized), 3 is_new (lossless), 4 score (raw) # after: 0 age (raw), 1 country (quantized), 2 is_new (lossless), 3 score (raw) # So feature_dim goes from 5 to 4, quantized_feature_indices from (2,) to (1,), and # lossless_feature_indices from (3,) to (2,). raw_feature_indices is derived from these, so it follows. weight_logical_index = metadata.raw_feature_indices[raw_column_offset] adjusted_quantized_feature_indices = _shift_indices_past_removed_column( metadata.quantized_feature_indices, weight_logical_index ) adjusted_lossless_feature_indices = _shift_indices_past_removed_column( metadata.lossless_feature_indices, weight_logical_index ) adjusted_metadata[edge_type] = replace( metadata, feature_dim=metadata.feature_dim - 1, quantized_feature_indices=adjusted_quantized_feature_indices, lossless_feature_indices=adjusted_lossless_feature_indices, ) if is_homogeneous: return adjusted_metadata[DEFAULT_HOMOGENEOUS_EDGE_TYPE] return adjusted_metadata
def _data_loading_process( tf_record_dataloader: TFRecordDataLoader, output_dict: MutableMapping[ str, Union[torch.Tensor, dict[Union[NodeType, EdgeType], torch.Tensor]] ], error_dict: MutableMapping[str, str], entity_type: str, serialized_tf_record_info: Union[ SerializedTFRecordInfo, dict[Union[NodeType, EdgeType], SerializedTFRecordInfo], ], rank: int, tf_dataset_options: TFDatasetOptions = TFDatasetOptions(), weight_edge_feat_name: Optional[Union[str, dict[EdgeType, str]]] = None, ) -> None: """ Spawned multiprocessing.Process which loads homogeneous or heterogeneous information for a specific entity type [node, edge, positive_label, negative_label] and moves to shared memory. Also logs timing information for duration of loading. If an exception is thrown, its traceback will be stored in the error_dict "error" field, since exceptions for spawned processes won't properly be raised to the parent process. Args: tf_record_dataloader (TFRecordDataLoader): TFRecordDataloader used for loading tensors from serialized tfrecords output_dict (MutableMapping[str, Union[torch.Tensor, dict[Union[NodeType, EdgeType], torch.Tensor]]]): Dictionary initialized by mp.Manager().dict() in which outputs of tensor loading will be written to error_dict (MutableMapping[str, str]): Dictionary initialized by mp.Manager().dict() in which error of errors in current process will be written to entity_type (str): Entity type to prefix ids, features, and error keys with when writing to the output_dict and error_dict fields serialized_tf_record_info (Union[SerializedTFRecordInfo, dict[NodeType, SerializedTFRecordInfo], dict[EdgeType, SerializedTFRecordInfo]]): Serialized information for current entity rank (int): Rank of the current machine tf_dataset_options (TFDatasetOptions): The options to use when building the dataset. weight_edge_feat_name (Optional[Union[str, dict[EdgeType, str]]]): Only used when ``entity_type == _EDGE_KEY``. Name of the edge feature column to extract as sampling weights. Ignored for node, positive_label, and negative_label entities. Supply a single string for homogeneous graphs or a per-edge-type dict for heterogeneous graphs. """ # We add a try - except clause here to ensure that exceptions are properly circulated back to the parent process try: # To simplify the logic to proceed on a singular code path, we convert homogeneous inputs to heterogeneous just within the scope of this function if isinstance(serialized_tf_record_info, SerializedTFRecordInfo): serialized_tf_record_info = ( {DEFAULT_HOMOGENEOUS_NODE_TYPE: serialized_tf_record_info} if serialized_tf_record_info.is_node_entity else {DEFAULT_HOMOGENEOUS_EDGE_TYPE: serialized_tf_record_info} ) is_input_homogeneous = True else: is_input_homogeneous = False all_tf_record_uris = [ serialized_entity.tfrecord_uri_prefix.uri for serialized_entity in serialized_tf_record_info.values() ] start_time = time.time() logger.info( f"Rank {rank} has begun to load data from tfrecord directories: {all_tf_record_uris}" ) ids: dict[Union[NodeType, EdgeType], torch.Tensor] = {} features: dict[Union[NodeType, EdgeType], torch.Tensor] = {} packed_features: dict[Union[NodeType, EdgeType], torch.Tensor] = {} labels: dict[Union[NodeType, EdgeType], torch.Tensor] = {} weights: dict[Union[NodeType, EdgeType], torch.Tensor] = {} for ( graph_type, serialized_entity_tf_record_info, ) in serialized_tf_record_info.items(): # We currently do not support training with labels for edge entities if ( serialized_entity_tf_record_info.label_keys and not serialized_entity_tf_record_info.is_node_entity ): raise NotImplementedError( "Label keys are not supported for edge entities" ) loaded_entity = tf_record_dataloader.load_as_torch_tensors( serialized_tf_record_info=serialized_entity_tf_record_info, tf_dataset_options=tf_dataset_options, ) entity_ids = loaded_entity.ids entity_features = loaded_entity.features entity_packed_features = loaded_entity.packed_features entity_labels = loaded_entity.labels ids[graph_type] = entity_ids logger.info( f"Rank {rank} finished loading {entity_type} ids of shape {entity_ids.shape} for graph type {graph_type} from {serialized_entity_tf_record_info.tfrecord_uri_prefix.uri}" ) if entity_features is not None: features[graph_type] = entity_features logger.info( f"Rank {rank} finished loading {entity_type} features of shape {entity_features.shape} for graph type {graph_type} from {serialized_entity_tf_record_info.tfrecord_uri_prefix.uri}" ) else: logger.info( f"Rank {rank} did not detect {entity_type} features for graph type {graph_type} from {serialized_entity_tf_record_info.tfrecord_uri_prefix.uri}" ) if entity_packed_features is not None: packed_features[graph_type] = entity_packed_features logger.info( f"Rank {rank} finished loading {entity_type} packed features of shape {entity_packed_features.shape} for graph type {graph_type} from {serialized_entity_tf_record_info.tfrecord_uri_prefix.uri}" ) else: logger.info( f"Rank {rank} did not detect {entity_type} packed features for graph type {graph_type} from {serialized_entity_tf_record_info.tfrecord_uri_prefix.uri}" ) if entity_labels is not None: labels[graph_type] = entity_labels logger.info( f"Rank {rank} finished loading {entity_type} labels of shape {entity_labels.shape} for graph type {graph_type} from {serialized_entity_tf_record_info.tfrecord_uri_prefix.uri}" ) else: logger.info( f"Rank {rank} did not detect {entity_type} labels for graph type {graph_type} from {serialized_entity_tf_record_info.tfrecord_uri_prefix.uri}" ) # Extract weight column from edge features when weight_edge_feat_name is set. # The weight column is sliced out of each edge type's feature tensor and stored # separately so it is not duplicated in the feature matrix. if weight_edge_feat_name is not None and entity_type == _EDGE_KEY: if isinstance(weight_edge_feat_name, str): if len(serialized_tf_record_info) != 1 or len(features) != 1: raise ValueError( f"weight_edge_feat_name must be a dict[EdgeType, str] for heterogeneous " f"graphs with multiple edge types ({sorted(serialized_tf_record_info)}). " "Provide an explicit per-edge-type mapping instead of a single string." ) col_name = weight_edge_feat_name edge_type, feat_tensor = next(iter(features.items())) assert isinstance(edge_type, EdgeType) feature_keys = list(serialized_tf_record_info[edge_type].feature_keys) weights[edge_type], trimmed = _extract_weight_col( feat_tensor, feature_keys, serialized_tf_record_info[edge_type].feature_spec, col_name, edge_type, ) if trimmed is not None: features[edge_type] = trimmed else: del features[edge_type] logger.info( f"Rank {rank} extracted weight column '{col_name}' " f"from {entity_type} features for type {edge_type}" ) else: # Iterate the EdgeType-keyed dict directly to stay within EdgeType. for edge_type, col_name in weight_edge_feat_name.items(): if edge_type not in features: continue feat_tensor = features[edge_type] feature_keys = list( serialized_tf_record_info[edge_type].feature_keys ) weights[edge_type], trimmed = _extract_weight_col( feat_tensor, feature_keys, serialized_tf_record_info[edge_type].feature_spec, col_name, edge_type, ) if trimmed is not None: features[edge_type] = trimmed else: del features[edge_type] logger.info( f"Rank {rank} extracted weight column '{col_name}' " f"from {entity_type} features for type {edge_type}" ) logger.info( f"Rank {rank} is attempting to share {entity_type} id memory for tfrecord directories: {all_tf_record_uris}" ) share_memory(ids) # We convert the ids back to homogeneous from the default heterogeneous setup if our provided input was homogeneous if features: logger.info( f"Rank {rank} is attempting to share {entity_type} feature memory for tfrecord directories: {all_tf_record_uris}" ) share_memory(features) # We convert the features back to homogeneous from the default heterogeneous setup if our provided input was homogeneous if packed_features: logger.info( f"Rank {rank} is attempting to share {entity_type} packed feature memory for tfrecord directories: {all_tf_record_uris}" ) share_memory(packed_features) if labels: logger.info( f"Rank {rank} is attempting to share {entity_type} label memory for tfrecord directories: {all_tf_record_uris}" ) share_memory(labels) if weights: logger.info( f"Rank {rank} is attempting to share {entity_type} weight memory for tfrecord directories: {all_tf_record_uris}" ) share_memory(weights) output_dict[_ID_FMT.format(entity=entity_type)] = ( list(ids.values())[0] if is_input_homogeneous else ids ) if features: output_dict[_FEATURE_FMT.format(entity=entity_type)] = ( list(features.values())[0] if is_input_homogeneous else features ) if packed_features: output_dict[_PACKED_FEATURE_FMT.format(entity=entity_type)] = ( list(packed_features.values())[0] if is_input_homogeneous else packed_features ) if labels: output_dict[_LABEL_FMT.format(entity=entity_type)] = ( list(labels.values())[0] if is_input_homogeneous else labels ) if weights: output_dict[_EDGE_WEIGHTS_KEY] = ( list(weights.values())[0] if is_input_homogeneous else weights ) logger.info( f"Rank {rank} has finished loading {entity_type} data from tfrecord directories: {all_tf_record_uris}, elapsed time: {time.time() - start_time:.2f} seconds" ) except Exception: error_dict[entity_type] = traceback.format_exc() # Node features are keyed by NodeType and edge features by EdgeType. A constrained TypeVar keeps that key type # through the helper: ``dict`` is invariant in its key, so a dict keyed by NodeType is not a dict keyed by # ``Union[NodeType, EdgeType]``. _EntityKey = TypeVar("_EntityKey", NodeType, EdgeType) class _EntityFeatures(NamedTuple, Generic[_EntityKey]): """Raw features, packed features and quantization metadata of one entity type.""" features: Optional[Union[torch.Tensor, dict[_EntityKey, torch.Tensor]]] packed_features: Optional[Union[torch.Tensor, dict[_EntityKey, torch.Tensor]]] metadata: Optional[ Union[ FeatureQuantizationMetadata, dict[_EntityKey, FeatureQuantizationMetadata], ] ] def _read_and_compress_features( output_dict: Mapping[str, Union[torch.Tensor, dict[_EntityKey, torch.Tensor]]], error_dict: Mapping[str, str], entity_type: str, metadata: Optional[ Union[ FeatureQuantizationMetadata, dict[_EntityKey, FeatureQuantizationMetadata], ] ], should_compress: bool, is_homogeneous: bool, default_key: _EntityKey, ) -> _EntityFeatures[_EntityKey]: """Reads the raw and packed features of one entity type once its loading process has joined. Compresses them with ``compress_loaded_features`` if ``should_compress`` is set and this entity type's own loading process did not fail. If this entity type's load failed, compression is skipped so the caller raises the stored load error once all loading processes have joined. The other ranks wait until the RPC timeout either way, as at the load barrier. Args: output_dict: Outputs of the loading process of ``entity_type``. error_dict: Tracebacks of the loading processes that failed, keyed by entity type. entity_type: Entity type whose features are read. metadata: Quantization metadata describing the packed features. should_compress: Whether to compress the features. is_homogeneous: Whether the graph is homogeneous, which decides the form of ``metadata``. default_key: Key the homogeneous graph's single node or edge type uses when tensors are keyed. Returns: The raw features, packed features and metadata, after compression if it ran. """ features = output_dict.get(_FEATURE_FMT.format(entity=entity_type), None) packed_features = output_dict.get( _PACKED_FEATURE_FMT.format(entity=entity_type), None ) # A failed load wrote no output, so compressing would raise a second, confusing error instead of the stored # load error that the caller raises after all joins. Only this entity's failure is checked: another entity's # process can fail at any time while this one compresses. if not should_compress or entity_type in error_dict: return _EntityFeatures(features, packed_features, metadata) features, packed_features, metadata = compress_loaded_features( features=features, quantized_features=packed_features, metadata=metadata, is_homogeneous=is_homogeneous, default_key=default_key, ) return _EntityFeatures(features, packed_features, metadata)
[docs] def load_torch_tensors_from_tf_record( tf_record_dataloader: TFRecordDataLoader, serialized_graph_metadata: SerializedGraphMetadata, should_load_tensors_in_parallel: bool, rank: int = 0, node_tf_dataset_options: TFDatasetOptions = TFDatasetOptions(), edge_tf_dataset_options: TFDatasetOptions = TFDatasetOptions(), weight_edge_feat_name: Optional[Union[str, dict[EdgeType, str]]] = None, ) -> LoadedGraphTensors: """ Loads all torch tensors from a SerializedGraphMetadata object for all entity [node, edge, positive_label, negative_label] and edge / node types. Running these processes in parallel slows the runtime of each individual process, but may still result in a net speedup across all entity types. As a result, there is a tradeoff that needs to be made between parallel and sequential tensor loading, which is why we don't parallelize across node and edge types. We enable the `should_load_tensors_in_parallel` to allow some customization for loading strategies based on the input data. If RPC is initialized, node and edge feature columns are losslessly compressed, each right after its own loading process joins, unless the ``GIGL_FEATURE_COMPRESSION`` environment variable is ``off`` (see :mod:`gigl.common.utils.feature_quantization.lossless_compression`). When loading in parallel, node compression runs while the edge loading process is still reading. The sampling-weight edge feature is removed from the edge features before they are compressed, so it is never compressed. With compression on and RPC initialized, every rank must call this function, with the same ``should_load_tensors_in_parallel``, because compression joins a collective RPC ``all_gather``. Without RPC, features are returned uncompressed. Args: tf_record_dataloader (TFRecordDataLoader): TFRecordDataloader used for loading tensors from serialized tfrecords serialized_graph_metadata (SerializedGraphMetadata): Serialized graph metadata contained serialized information for loading tfrecords across node and edge types should_load_tensors_in_parallel (bool): Whether tensors should be loaded from serialized information in parallel or in sequence across the [node, edge, pos_label, neg_label] entity types. rank (int): Rank on current machine node_tf_dataset_options (TFDatasetOptions): The options to use for nodes when building the dataset. edge_tf_dataset_options (TFDatasetOptions): The options to use for edges when building the dataset. weight_edge_feat_name (Optional[Union[str, dict[EdgeType, str]]]): Name of the edge feature column to extract as sampling weights. The column is removed from the edge feature matrix and returned separately via ``LoadedGraphTensors.edge_weights``. Supply a single string for homogeneous graphs or a per-edge-type dict for heterogeneous graphs. Returns: loaded_graph_tensors (LoadedGraphTensors): Unpartitioned Graph Tensors, with the quantization metadata that describes their packed features. Raises: ValueError: If a loading process failed, or if ``GIGL_FEATURE_COMPRESSION`` holds an unknown value while RPC is initialized. """ _validate_weight_edge_feature_name( edge_entity_info=serialized_graph_metadata.edge_entity_info, weight_edge_feat_name=weight_edge_feat_name, ) should_compress = ( rpc_is_initialized() and resolve_feature_compression() == FeatureCompression.AUTO ) node_is_homogeneous = isinstance( serialized_graph_metadata.node_entity_info, SerializedTFRecordInfo ) edge_is_homogeneous = isinstance( serialized_graph_metadata.edge_entity_info, SerializedTFRecordInfo ) node_quantization_metadata = serialized_graph_metadata.node_quantization_metadata edge_quantization_metadata = remove_sampling_weight_from_edge_quantization_metadata( serialized_graph_metadata=serialized_graph_metadata, weight_edge_feat_name=weight_edge_feat_name, ) logger.info(f"Rank {rank} starting loading torch tensors from serialized info ...") start_time = time.time() manager = Manager() # By default, torch processes are created using the `fork` method, which makes a copy of the entire process. This can be problematic in multi-threaded settings, # especially when working with TensorFlow, since this includes all threads, which can lead to deadlocks or other synchronization issues. As a result, we set the # start method to spawn, which creates a new Python interpreter process and is much safer with multi-threading applications. ctx = mp.get_context("spawn") node_output_dict: MutableMapping[ str, Union[torch.Tensor, dict[NodeType, torch.Tensor]] ] = manager.dict() edge_output_dict: MutableMapping[ str, Union[torch.Tensor, dict[EdgeType, torch.Tensor]] ] = manager.dict() error_dict: MutableMapping[str, str] = manager.dict() node_data_loading_process = ctx.Process( target=_data_loading_process, kwargs={ "tf_record_dataloader": tf_record_dataloader, "output_dict": node_output_dict, "error_dict": error_dict, "entity_type": _NODE_KEY, "serialized_tf_record_info": serialized_graph_metadata.node_entity_info, "rank": rank, "tf_dataset_options": node_tf_dataset_options, }, ) edge_data_loading_process = ctx.Process( target=_data_loading_process, kwargs={ "tf_record_dataloader": tf_record_dataloader, "output_dict": edge_output_dict, "error_dict": error_dict, "entity_type": _EDGE_KEY, "serialized_tf_record_info": serialized_graph_metadata.edge_entity_info, "rank": rank, "tf_dataset_options": edge_tf_dataset_options, "weight_edge_feat_name": weight_edge_feat_name, }, ) if serialized_graph_metadata.positive_label_entity_info is not None: positive_label_data_loading_process = ctx.Process( target=_data_loading_process, kwargs={ "tf_record_dataloader": tf_record_dataloader, "output_dict": edge_output_dict, "error_dict": error_dict, "entity_type": _POSITIVE_LABEL_KEY, "serialized_tf_record_info": serialized_graph_metadata.positive_label_entity_info, "rank": rank, }, ) else: logger.info(f"No positive labels detected from input data") if serialized_graph_metadata.negative_label_entity_info is not None: negative_label_data_loading_process = ctx.Process( target=_data_loading_process, kwargs={ "tf_record_dataloader": tf_record_dataloader, "output_dict": edge_output_dict, "error_dict": error_dict, "entity_type": _NEGATIVE_LABEL_KEY, "serialized_tf_record_info": serialized_graph_metadata.negative_label_entity_info, "rank": rank, }, ) else: logger.info(f"No negative labels detected from input data") if should_load_tensors_in_parallel: # In this setting, we start all the processes at once and join them at the end to achieve parallelized tensor loading logger.info("Loading Serialized TFRecord Data in Parallel ...") node_data_loading_process.start() edge_data_loading_process.start() if serialized_graph_metadata.positive_label_entity_info is not None: positive_label_data_loading_process.start() if serialized_graph_metadata.negative_label_entity_info is not None: negative_label_data_loading_process.start() node_data_loading_process.join() # We do this because in practice nodes get loaded quicker, and we can get a head start on the compression # while the edge loading process is still reading. node_features, node_packed_features, node_quantization_metadata = ( _read_and_compress_features( output_dict=node_output_dict, error_dict=error_dict, entity_type=_NODE_KEY, metadata=node_quantization_metadata, should_compress=should_compress, is_homogeneous=node_is_homogeneous, default_key=DEFAULT_HOMOGENEOUS_NODE_TYPE, ) ) edge_data_loading_process.join() edge_features, edge_packed_features, edge_quantization_metadata = ( _read_and_compress_features( output_dict=edge_output_dict, error_dict=error_dict, entity_type=_EDGE_KEY, metadata=edge_quantization_metadata, should_compress=should_compress, is_homogeneous=edge_is_homogeneous, default_key=DEFAULT_HOMOGENEOUS_EDGE_TYPE, ) ) if serialized_graph_metadata.positive_label_entity_info is not None: positive_label_data_loading_process.join() if serialized_graph_metadata.negative_label_entity_info is not None: negative_label_data_loading_process.join() else: # In this setting, we start and join each process one-at-a-time in order to achieve sequential tensor loading logger.info("Loading Serialized TFRecord Data in Sequence ...") # Here we launch edge loading process first since experimentally # we have found that, since edge data is larger than node data, # launching edge before launching node reduces peak memory usage # compared with first launching node loading and then launching edge loading. edge_data_loading_process.start() edge_data_loading_process.join() edge_features, edge_packed_features, edge_quantization_metadata = ( _read_and_compress_features( output_dict=edge_output_dict, error_dict=error_dict, entity_type=_EDGE_KEY, metadata=edge_quantization_metadata, should_compress=should_compress, is_homogeneous=edge_is_homogeneous, default_key=DEFAULT_HOMOGENEOUS_EDGE_TYPE, ) ) node_data_loading_process.start() node_data_loading_process.join() node_features, node_packed_features, node_quantization_metadata = ( _read_and_compress_features( output_dict=node_output_dict, error_dict=error_dict, entity_type=_NODE_KEY, metadata=node_quantization_metadata, should_compress=should_compress, is_homogeneous=node_is_homogeneous, default_key=DEFAULT_HOMOGENEOUS_NODE_TYPE, ) ) if serialized_graph_metadata.positive_label_entity_info is not None: positive_label_data_loading_process.start() positive_label_data_loading_process.join() if serialized_graph_metadata.negative_label_entity_info is not None: negative_label_data_loading_process.start() negative_label_data_loading_process.join() if error_dict: for entity_type, traceback in error_dict.items(): logger.error( f"Identified error in {entity_type} data loading process: \n{traceback}" ) raise ValueError( f"Raised error in data loading processes for entity types {error_dict.keys()}." ) node_ids = node_output_dict[_ID_FMT.format(entity=_NODE_KEY)] node_labels = node_output_dict.get(_LABEL_FMT.format(entity=_NODE_KEY), None) edge_index = edge_output_dict[_ID_FMT.format(entity=_EDGE_KEY)] edge_weights = edge_output_dict.get(_EDGE_WEIGHTS_KEY, None) positive_labels = edge_output_dict.get( _ID_FMT.format(entity=_POSITIVE_LABEL_KEY), None ) negative_labels = edge_output_dict.get( _ID_FMT.format(entity=_NEGATIVE_LABEL_KEY), None ) if rpc_is_initialized(): logger.info( f"Rank {rank} has finished loading data in {time.time() - start_time:.2f} seconds. Wait for other ranks to finish loading data from tfrecords" ) barrier() logger.info( f"All ranks have finished loading data from tfrecords, rank {rank} finished in {time.time() - start_time:.2f} seconds" ) return LoadedGraphTensors( node_ids=node_ids, node_features=node_features, node_packed_features=node_packed_features, node_labels=node_labels, edge_index=edge_index, edge_features=edge_features, edge_packed_features=edge_packed_features, positive_label=positive_labels, negative_label=negative_labels, edge_weights=edge_weights, node_quantization_metadata=node_quantization_metadata, edge_quantization_metadata=edge_quantization_metadata, )