From 9e9ea2a0c34091ea8ada3ce7c996616b7968c434 Mon Sep 17 00:00:00 2001 From: Theodore Vasiloudis Date: Tue, 30 Jul 2024 02:39:10 +0300 Subject: [PATCH 1/4] [GSProcessing] Update EMRS image to 7.1.0, add file in image to ensure we recognize execution env. (#943) *Issue #, if available:* *Description of changes:* * We update the source image for EMRS to use the 7.1.0 image * We add a file in the image to ensure we correctly identify the execution environment. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --- .../docker/0.3.1/emr-serverless/Dockerfile.cpu | 4 +++- .../graphstorm_processing/distributed_executor.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/graphstorm-processing/docker/0.3.1/emr-serverless/Dockerfile.cpu b/graphstorm-processing/docker/0.3.1/emr-serverless/Dockerfile.cpu index b3b511f6b4..e8db91b4bc 100644 --- a/graphstorm-processing/docker/0.3.1/emr-serverless/Dockerfile.cpu +++ b/graphstorm-processing/docker/0.3.1/emr-serverless/Dockerfile.cpu @@ -1,5 +1,5 @@ ARG ARCH=x86_64 -FROM public.ecr.aws/emr-serverless/spark/emr-7.0.0:20240206-${ARCH} as base +FROM public.ecr.aws/emr-serverless/spark/emr-7.1.0:20240528-${ARCH} as base USER root ENV PYTHON_VERSION=3.9.18 @@ -40,6 +40,8 @@ else \ python3 -c "from transformers import AutoModel; AutoModel.from_pretrained('${MODEL}')"; \ fi +# We use this file as an indicator of the execution environment +RUN touch /usr/lib/spark/code/EMR_SERVERLESS_EXECUTION # GSProcessing codebase COPY code/ /usr/lib/spark/code/ diff --git a/graphstorm-processing/graphstorm_processing/distributed_executor.py b/graphstorm-processing/graphstorm_processing/distributed_executor.py index 0b2e6e5b21..c374056f56 100644 --- a/graphstorm-processing/graphstorm_processing/distributed_executor.py +++ b/graphstorm-processing/graphstorm_processing/distributed_executor.py @@ -573,10 +573,10 @@ def main(): format="[GSPROCESSING] %(asctime)s %(levelname)-8s %(message)s", ) - # Determine if we're running within a SageMaker container + # Determine execution environment if os.path.exists("/opt/ml/config/processingjobconfig.json"): execution_env = ExecutionEnv.SAGEMAKER - elif os.path.exists("/emr-serverless-config.json"): + elif os.path.exists("/usr/lib/spark/code/EMR_SERVERLESS_EXECUTION"): execution_env = ExecutionEnv.EMR_SERVERLESS elif os.path.exists("/usr/lib/spark/code/EMR_EXECUTION"): execution_env = ExecutionEnv.EMR_ON_EC2 From f4d578560e6eee6b991b133de6a9f97005dab232 Mon Sep 17 00:00:00 2001 From: "Jian Zhang (James)" <6593865@qq.com> Date: Mon, 29 Jul 2024 17:37:31 -0700 Subject: [PATCH 2/4] [Doc] API doc string refactor for graphstorm.dataloading (#934) *Issue #, if available:* *Description of changes:* This PR refactors the API doc string and the API reference rst pages for graphstorm.dataloading module. The rendered readthedoc pages are: 1. API reference index page: https://james4graphstorm.readthedocs.io/en/james_apidoc_dataloading/api/references/index.html 2. graphstorm.dataset and graphstorm.dataloading index page: https://james4graphstorm.readthedocs.io/en/james_apidoc_dataloading/api/references/graphstorm.dataloading.html All rst pages of classes under graphstorm.dataloading module are updated. Back compatibility breaking changes: 1. Rename the `pos_graph_feat_fields` with `pos_graph_edge_feat_fields` to make its meaning clearer. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Ubuntu Co-authored-by: Oxfordblue7 <457620544@qq.com> Co-authored-by: xiang song(charlie.song) --- docs/source/_templates/dataloadertemplate.rst | 4 +- docs/source/_templates/datasettemplate.rst | 3 +- .../api/references/graphstorm.dataloading.rst | 46 +- docs/source/graph-construction/index.rst | 2 + python/graphstorm/dataloading/dataloading.py | 736 ++++++++++-------- python/graphstorm/dataloading/dataset.py | 148 ++-- python/graphstorm/trainer/lp_trainer.py | 4 +- python/graphstorm/trainer/mt_trainer.py | 4 +- 8 files changed, 515 insertions(+), 432 deletions(-) diff --git a/docs/source/_templates/dataloadertemplate.rst b/docs/source/_templates/dataloadertemplate.rst index f02d586215..515139624d 100644 --- a/docs/source/_templates/dataloadertemplate.rst +++ b/docs/source/_templates/dataloadertemplate.rst @@ -7,4 +7,6 @@ .. autoclass:: {{ name }} :show-inheritance: - :special-members: __iter__, __next__ \ No newline at end of file + :members: + :member-order: bysource + :special-members: __iter__, __next__, __len__ diff --git a/docs/source/_templates/datasettemplate.rst b/docs/source/_templates/datasettemplate.rst index 1225aa3c46..16e04ade54 100644 --- a/docs/source/_templates/datasettemplate.rst +++ b/docs/source/_templates/datasettemplate.rst @@ -7,4 +7,5 @@ .. autoclass:: {{ name }} :show-inheritance: - :members: prepare_data, get_node_feats, get_edge_feats, get_labels, get_node_feat_size + :members: + :member-order: bysource diff --git a/docs/source/api/references/graphstorm.dataloading.rst b/docs/source/api/references/graphstorm.dataloading.rst index 2ce324888c..db95907702 100644 --- a/docs/source/api/references/graphstorm.dataloading.rst +++ b/docs/source/api/references/graphstorm.dataloading.rst @@ -1,17 +1,34 @@ .. _apidataloading: -graphstorm.dataloading -========================== +graphstorm.dataloading.dataset +=============================== - GraphStorm dataloading module includes a set of graph DataSets and DataLoaders for different - graph machine learning tasks. - - If users would like to customize DataLoaders, please extend those classes in the - :ref:`Base DataLoaders ` section and customize their abstract methods. + GraphStorm dataset provides one unified dataset class, i.e., ``GSgnnData``, for all graph + machine learning tasks. Users can build a ``GSgnnData`` object by giving the path of + the JSON file created by the :ref:`GraphStorm Graph Construction` + operations. The ``GSgnnData`` will load the related graph artifacts specified in the JSON + file. It provides a set of APIs for users to extract information of the graph data for + model training and inference. .. currentmodule:: graphstorm.dataloading -.. _basedataloaders: +.. autosummary:: + :toctree: ../generated/ + :nosignatures: + :template: datasettemplate.rst + + GSgnnData + +graphstorm.dataloading.dataloading +=================================== + + GraphStorm dataloading module includes a set of different DataLoaders for + different graph machine learning tasks. + + If users would like to customize DataLoaders, please extend those dataloader base + classes in the **Base DataLoaders** section and customize their abstract functions. + +.. currentmodule:: graphstorm.dataloading Base DataLoaders ------------------- @@ -25,16 +42,6 @@ Base DataLoaders GSgnnEdgeDataLoaderBase GSgnnLinkPredictionDataLoaderBase -DataSets ------------- - -.. autosummary:: - :toctree: ../generated/ - :nosignatures: - :template: datasettemplate.rst - - GSgnnData - DataLoaders ------------ @@ -44,5 +51,8 @@ DataLoaders :template: dataloadertemplate.rst GSgnnNodeDataLoader + GSgnnNodeSemiSupDataLoader GSgnnEdgeDataLoader GSgnnLinkPredictionDataLoader + GSgnnLinkPredictionTestDataLoader + GSgnnLinkPredictionPredefinedTestDataLoader diff --git a/docs/source/graph-construction/index.rst b/docs/source/graph-construction/index.rst index b917c814f6..4c5d8403f5 100644 --- a/docs/source/graph-construction/index.rst +++ b/docs/source/graph-construction/index.rst @@ -1,3 +1,5 @@ +.. _graph_construction: + ================== Graph Construction ================== diff --git a/python/graphstorm/dataloading/dataloading.py b/python/graphstorm/dataloading/dataloading.py index 1020529857..d3c9728770 100644 --- a/python/graphstorm/dataloading/dataloading.py +++ b/python/graphstorm/dataloading/dataloading.py @@ -117,7 +117,7 @@ class MultiLayerNeighborSamplerForReconstruct(dgl.dataloading.BlockSampler): construct_feat_ntype : list of str The node types that requires to construct node features. construct_feat_fanout : int - The fanout required to construct node features. + The fanout used when constructing node features for feature-less nodes. """ def __init__(self, sampler, dataset, construct_feat_ntype, construct_feat_fanout): super().__init__() @@ -151,38 +151,45 @@ def sample_blocks(self, g, seed_nodes, exclude_eids=None): class GSgnnEdgeDataLoaderBase(): """ The base dataloader class for edge tasks. - If users want to customize the dataloader for edge prediction tasks + If users want to customize dataloaders for edge prediction tasks, they should extend this base class by implementing the special methods - `__iter__` and `__next__`. + ``__iter__``, ``__next__``, and ``__len__``. Parameters ---------- dataset : GSgnnData - The dataset for the edge task. + The GraphStorm data for edge tasks. target_idx : dict of Tensors - The target edge IDs. + The target edge indexes for prediction. fanout : list or dict of lists - The fanout for each GNN layer. + The fanout for each GNN layer. If it's a dict of lists, it indicates the fanout for each + edge type. label_field: str or dict of str Label field of the edge task. - node_feats: str, or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. - Default: None - edge_feats: str, or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None - decoder_edge_feats: str or dict of list of str - Edge features used in decoder. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None + node_feats: str, or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + + Default: None. + edge_feats: str, or dict of list of str + Edge feature fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. + decoder_edge_feats: str, or dict of list of str + Edge feature fileds used in edge decoders in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. """ def __init__(self, dataset, target_idx, fanout, label_field, node_feats=None, edge_feats=None, decoder_edge_feats=None): @@ -199,154 +206,168 @@ def __init__(self, dataset, target_idx, fanout, self._decoder_edge_feats = decoder_edge_feats def __iter__(self): - """ Returns an iterator object + """ Returns an iterator object. """ def __next__(self): """ Return a mini-batch data for the edge task. - A mini-batch comprises three objects: the input node IDs, - the target edges and the subgraph blocks for message passing. + A mini-batch comprises three objects: 1) the input node IDs, + 2) the target edges, and 3) the subgraph blocks for message passing. Returns ------- - dict of Tensors : the input node IDs of the mini-batch. - DGLGraph : the target edges. - list of DGLGraph : the subgraph blocks for message passing. + + - dict of Tensors : the input node IDs of the mini-batch. + - DGLGraph : the target edges. + - list of DGLGraph : the subgraph blocks for message passing. + """ def __len__(self): - """ Return the length (number of mini-batches) of the data loader + """ Return the length (number of mini-batches) of the data loader. Returns + ------- int: length """ @property def data(self): - """ The dataset of this dataloader. + """ The dataset of this dataloader, which is given in class initialization. Returns ------- - GSgnnData : The dataset of the dataloader. + GSgnnData: The dataset of the dataloader. """ return self._data @property def target_eidx(self): - """ Target edge idx for prediction + """ Target edge indexes for prediction, which is given in class initialization. Returns ------- - dict of Tensors : the target edge IDs. + dict of Tensors: the target edge IDs, which is given in class initialization. """ return self._target_eidx @property def fanout(self): - """ The fan out of each GNN layers + """ The fan out of each GNN layers, which is given in class initialization. Returns ------- - list or a dict of list : the fanouts for each GNN layer. + list or a dict of list: the fanouts for each GNN layer, which is given in class + initialization. """ return self._fanout @property def label_field(self): - """ The label field + """ The label field, which is given in class initialization. Returns ------- - str: Label fields in the graph. + str: Label fields in the graph, which is given in class initialization. """ return self._label_field @property def node_feat_fields(self): - """ Node features + """ Node feature fields, which is given in class initialization. Returns ------- - str or dict of list of str: Node feature fields in the graph. + str or dict of list of str: Node feature fields in the graph, which is given in class + initialization. """ return self._node_feats @property def edge_feat_fields(self): - """ Edge features + """ Edge feature fields, which is given in class initialization. Returns ------- - str or dict of list of str: Node feature fields in the graph. + str or dict of list of str: Node feature fields in the graph, which is given in class + initialization. """ return self._edge_feats @property def decoder_edge_feat_fields(self): - """ Edge features for edge decoder. + """ Edge features for edge decoder, which is given in class initialization. Returns ------- - str or dict of list of str: Node feature fields in the graph. + str or dict of list of str: Node feature fields in the graph, which is given in class + initialization. """ return self._decoder_edge_feats class GSgnnEdgeDataLoader(GSgnnEdgeDataLoaderBase): - """ The minibatch dataloader for edge prediction + """ The mini-batch dataloader for edge prediction tasks. - GSgnnEdgeDataLoader samples GraphStorm edge dataset into an iterable over mini-batches - of samples. Both source and destination nodes are included in the batch_graph, which + ``GSgnnEdgeDataLoader`` samples target edges into an iterable over mini-batches + of samples. Both source and destination nodes are included in the ``batch_graph``, which will be used by GraphStorm Trainers and Inferrers. Parameters ------------ dataset: GSgnnData - The GraphStorm edge dataset + The GraphStorm data. target_idx : dict of Tensors - The target edges for prediction - fanout: list of int or dict of list - Neighbor sample fanout. If it's a dict, it indicates the fanout for each edge type. + The target edge indexes for prediction. + fanout: list of int, or dict of list + Neighbor sampling fanout. If it's a dict of list, it indicates the fanout for each + edge type. batch_size: int - Batch size + Mini-batch size. label_field: str or dict of str Label field of the edge task. - node_feats: str, or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. - Default: None - edge_feats: str, or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None - decoder_edge_feats: str or dict of list of str - Edge features used in decoder. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None + node_feats: str, or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + + Default: None. + edge_feats: str, or dict of list of str + Edge features fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. + decoder_edge_feats: str, or dict of list of str + Edge features used in edge decoders in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. train_task : bool - Whether or not for training. + Whether or not is the dataloader for training. reverse_edge_types_map: dict - A map for reverse edge type + A map for reverse edge type. exclude_training_targets: bool - Whether to exclude training edges during neighbor sampling + Whether to exclude training edges during neighbor sampling. remove_target_edge_type: bool - Whether we will exclude all edges of the target edge type in message passing. + Whether to exclude all edges of the target edge type in message passing. construct_feat_ntype : list of str The node types that requires to construct node features. construct_feat_fanout : int - The fanout required to construct node features. + The fanout used when constructing node features for feature-less nodes. Examples ------------ To train a 2-layer GNN for edge prediction on a set of edges ``target_idx`` on - a graph where each nodes takes messages from 15 neighbors on the first layer - and 10 neighbors on the second. + a graph where each edge (source and destination node pair) takes messages from 15 + neighbors on the first layer and 10 neighbors on the second. .. code:: python @@ -443,29 +464,17 @@ def __next__(self): return self.dataloader.__next__() def __len__(self): - # Follow - # https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116 - # In DGL, DistDataLoader.expected_idxs is the length (number of batches) - # of the datalaoder. - return self.dataloader.expected_idxs - - @property - def data(self): - """ The dataset of this dataloader. - """ - return self._data - - @property - def target_eidx(self): - """ Target edge idx for prediction """ - return self._target_eidx + Follow + https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116. + In DGL, ``DistDataLoader.expected_idxs`` is the length (number of batches) + of the dataloader. - @property - def fanout(self): - """ The fan out of each GNN layers + Returns: + -------- + int: The length (number of batches) of the dataloader. """ - return self._fanout + return self.dataloader.expected_idxs ################ Minibatch DataLoader (Link Prediction) ####################### @@ -483,36 +492,41 @@ def fanout(self): BUILTIN_LP_FIXED_NEG_SAMPLER = 'fixed' class GSgnnLinkPredictionDataLoaderBase(): - """ The base class of link prediction dataloader. + """ The base dataloader class for link prediction tasks. - If users want to customize the dataloader for link prediction tasks + If users want to customize dataloaders for link prediction tasks, they should extend this base class by implementing the special methods - `__iter__` and `__next__`. + ``__iter__``, ``__next__``, and ``__len__``. Parameters ---------- dataset: GSgnnData - The GraphStorm edge dataset + The GraphStorm data for link prediction tasks. target_idx : dict of Tensors - The target edges for prediction - fanout: list of int or dict of list - Neighbor sample fanout. If it's a dict, it indicates the fanout for each edge type. - node_feats: str, or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. - Default: None - edge_feats: str, or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None - pos_graph_edge_feats: str or dist of list of str + The target edge indexes for link prediction. + fanout: list of int, or dict of list + Neighbor sampling fanout. If it's a dict of list, it indicates the fanout for each + edge type. + node_feats: str, or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + + Default: None. + edge_feats: str, or dict of list of str + Edge feature fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. + pos_graph_edge_feats: str, or dict of list of str The field of the edge features used by positive graph in link prediction. - For example edge weight. - Default: None + For example edge weights. + Default: None. """ def __init__(self, dataset, target_idx, fanout, node_feats=None, edge_feats=None, pos_graph_edge_feats=None): @@ -527,36 +541,40 @@ def __init__(self, dataset, target_idx, fanout, self._pos_graph_edge_feats = pos_graph_edge_feats def __iter__(self): - """ Returns an iterator object + """ Returns an iterator object. """ def __next__(self): """ Return a mini-batch for link prediction. A mini-batch of link prediction contains four objects: - * the input node IDs of the mini-batch, - * the target positive edges for prediction, - * the negative edges for prediction, - * the subgraph blocks for message passing. + + - the input node IDs of the mini-batch. + - the target positive edges for prediction. + - the sampled negative edges for prediction. + - the subgraph blocks for message passing. Returns ------- - Tensor or dict of Tensors : the input nodes of a mini-batch. - DGLGraph : positive edges. - DGLGraph : negative edges. - list of DGLGraph : subgraph blocks for message passing. + + - Tensor or dict of Tensors: the input nodes of a mini-batch. + - DGLGraph: positive edges. + - DGLGraph: negative edges. + - list of DGLGraph: subgraph blocks for message passing. + """ def __len__(self): - """ Return the length (number of mini-batches) of the data loader + """ Return the length (number of mini-batches) of the data loader. Returns + ------- int: length """ @property def data(self): - """ The dataset of this dataloader. + """ The dataset of this dataloader, which is given in class initialization. Returns ------- @@ -566,7 +584,7 @@ def data(self): @property def fanout(self): - """ The fan out of each GNN layers + """ The fan out of each GNN layers, which is given in class initialization. Returns ------- @@ -576,7 +594,7 @@ def fanout(self): @property def target_eidx(self): - """ The target edges for prediction. + """ The target edge indexes for prediction, which is given in class initialization. Returns ------- @@ -586,7 +604,7 @@ def target_eidx(self): @property def node_feat_fields(self): - """ Node features + """ Node feature fields, which is given in class initialization. Returns ------- @@ -596,85 +614,92 @@ def node_feat_fields(self): @property def edge_feat_fields(self): - """ Edge features + """ Edge feature fields, which is given in class initialization. Returns ------- - str or dict of list of str: Node feature fields in the graph. + str or dict of list of str: Edge feature fields in the graph. """ return self._edge_feats @property - def pos_graph_feat_fields(self): - """ Get edge feature fields of positive graphs + def pos_graph_edge_feat_fields(self): + """ Get edge feature fields of positive graphs, which is given in class initialization. Returns ------- - str or dict of list of str: Node feature fields in the graph. + str or dict of list of str: Edge feature fields in the positive graph. """ return self._pos_graph_edge_feats class GSgnnLinkPredictionDataLoader(GSgnnLinkPredictionDataLoaderBase): - """ Link prediction minibatch dataloader + """ Mini-batch dataloader for link prediction. - GSgnnLinkPredictionDataLoader samples GraphStorm edge dataset into an iterable over mini-batches - of samples. In each batch, pos_graph and neg_graph are sampled subgraph for positive and - negative edges, which will be used by GraphStorm Trainers and Inferrers. Given a positive edge, - a negative edge is composed of the source node and a random negative destination nodes - according to a uniform distribution. + ``GSgnnLinkPredictionDataLoader`` samples GraphStorm data into an iterable over mini-batches + of samples. In each batch, ``pos_graph`` and ``neg_graph`` are sampled subgraph for positive + and negative edges, which will be used by GraphStorm Trainers and Inferrers. + + Given a positive edge, a negative edge is composed of the source node and a random negative + destination nodes according to a uniform distribution. Argument -------- dataset: GSgnnData - The GraphStorm edge dataset + The GraphStorm data. target_idx : dict of Tensors - The target edges for prediction - fanout: list of int or dict of list - Neighbor sample fanout. If it's a dict, it indicates the fanout for each edge type. + The target edge indexes for prediction. + fanout: list of int, or dict of list + Neighbor sampling fanout. If it's a dict of list, it indicates the fanout for each + edge type. batch_size: int - Batch size + Mini-batch size. num_negative_edges: int - The number of negative edges per positive edge - node_feats: str, or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. - Default: None - edge_feats: str, or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None - pos_graph_edge_feats: str or dist of list of str - The field of the edge features used by positive graph in link prediction. + The number of negative edges per positive edge. + node_feats: str, or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + + Default: None. + edge_feats: str, or dict of list of str + Edge feature fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. + pos_graph_edge_feats: str, or dict of list of str + The edge feature fields used by positive graph in link prediction. For example edge weight. - Default: None + Default: None. train_task : bool - Whether or not for training. + Whether or not it is a dataloader for training. reverse_edge_types_map: dict - A map for reverse edge type + A map for reverse edge type. exclude_training_targets: bool - Whether to exclude training edges during neighbor sampling + Whether to exclude training edges during neighbor sampling. edge_mask_for_gnn_embeddings : str - The mask that indicates the edges used for computing GNN embeddings. By default, + The mask indicates the edges used for computing GNN embeddings. By default, the dataloader uses the edges in the training graphs to compute GNN embeddings to avoid information leak for link prediction. construct_feat_ntype : list of str The node types that requires to construct node features. construct_feat_fanout : int - The fanout required to construct node features. - edge_dst_negative_field: str or dict of str - The feature field(s) that store the hard negative edges for each edge type. - num_hard_negs: int or dict of int - The number of hard negatives per positive edge for each edge type + The fanout used when constructing node features for feature-less nodes. + edge_dst_negative_field: str, or dict of str + The feature fields that store the hard negative edges for each edge type. + num_hard_negs: int, or dict of int + The number of hard negatives per positive edge for each edge type. Examples ------------ To train a 2-layer GNN for link prediction on a set of positive edges ``target_idx`` on - a graph where each nodes takes messages from 15 neighbors on the first layer - and 10 neighbors on the second. We use 10 negative edges per positive in this example. + a graph where each edge (a source and destination node pair) takes messages from 15 neighbors + on the first layer and 10 neighbors on the second. + We use 10 negative edges per positive in this example. .. code:: python @@ -785,10 +810,16 @@ def __next__(self): return self.dataloader.__next__() def __len__(self): - # Follow - # https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116 - # In DGL, DistDataLoader.expected_idxs is the length (number of batches) - # of the datalaoder. + """ + Follow + https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116. + In DGL, ``DistDataLoader.expected_idxs`` is the length (number of batches) + of the dataloader. + + Returns: + -------- + int: The length (number of batches) of the dataloader. + """ return self.dataloader.expected_idxs class GSgnnLPJointNegDataLoader(GSgnnLinkPredictionDataLoader): @@ -929,7 +960,7 @@ def _prepare_negative_sampler(self, num_negative_edges): class AllEtypeDistEdgeDataLoader(DistDataLoader): """ Distributed edge data sampler that samples at least one - edge for each edge type in a minibatch + edge for each edge type in a mini-batch Parameters ---------- @@ -978,7 +1009,7 @@ def _reinit_dataset(self): bs_per_type = {} for etype, idxs in self.data_idx.items(): # compute the number of edges to be sampled for - # each edge type in a minibatch. + # each edge type in a mini-batch. # If batch_size * num_edges / total_edges < 0, then set 1. # # Note: The resulting batch size of a mini batch may be larger @@ -1066,7 +1097,7 @@ def _next_data(self): return new_ret class GSgnnAllEtypeLinkPredictionDataLoader(GSgnnLinkPredictionDataLoader): - """ Link prediction minibatch dataloader. In each minibatch, + """ Link prediction mini-batch dataloader. In each mini-batch, at least one edge is sampled from each etype. Note: using this dataloader with a graph with massive etypes @@ -1147,15 +1178,15 @@ def __next__(self): def __len__(self): # Follow - # https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116 + # https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116. # In DGL, DistDataLoader.expected_idxs is the length (number of batches) - # of the datalaoder. + # of the dataloader. # AllEtypeDistEdgeDataLoader is a child class of DistDataLoader. return self.dataloader.expected_idxs class GSgnnAllEtypeLPJointNegDataLoader(GSgnnAllEtypeLinkPredictionDataLoader): """ Link prediction dataloader with joint negative sampler. - In each minibatch, at least one edge is sampled from each etype. + In each mini-batch, at least one edge is sampled from each etype. """ @@ -1165,47 +1196,52 @@ def _prepare_negative_sampler(self, num_negative_edges): return negative_sampler class GSgnnLinkPredictionTestDataLoader(GSgnnLinkPredictionDataLoaderBase): - """ Link prediction minibatch dataloader for validation and test. + """ Mini-batch dataloader for link prediction validation and test. In order to efficiently compute positive and negative scores for - link prediction tasks, GSgnnLinkPredictionTestDataLoader is designed - to only generates edges, i.e., (src, dst) pairs. + link prediction tasks, ``GSgnnLinkPredictionTestDataLoader`` is designed + to only generates edges, i.e., source and destination node pairs. The negative edges are sampled uniformly. Parameters ----------- dataset: GSgnnData - The GraphStorm edge dataset + The GraphStorm data. target_idx : dict of Tensors - The target edges for prediction + The target edge indexes for link prediction. batch_size: int - Batch size + Mini-batch size. num_negative_edges: int - The number of negative edges per positive edge - fanout: int - Evaluation fanout for computing node embedding + The number of negative edges per positive edge. + fanout: list of int, or dict of list + Neighbor sampling fanout. If it's a dict of list, it indicates the fanout for each + edge type. fixed_test_size: int Fixed number of test data used in evaluation. If it is none, use the whole testset. - When test is huge, using fixed_test_size + When test is huge, using `fixed_test_size` can save validation and test time. Default: None. - node_feats: str, or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. - Default: None - edge_feats: str, or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None - pos_graph_edge_feats: str or dist of list of str - The field of the edge features used by positive graph in link prediction. + node_feats: str, or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + + Default: None. + edge_feats: str, or dict of list of str + Edge feature fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. + pos_graph_edge_feats: str or dict of list of str + The edge feature fields used by positive graph in link prediction. For example edge weight. - Default: None + Default: None. """ def __init__(self, dataset, target_idx, batch_size, num_negative_edges, fanout=None, fixed_test_size=None, @@ -1285,15 +1321,10 @@ def __len__(self): num_iters += math.ceil(test_size / self._batch_size) return num_iters - @property - def fanout(self): - """ Get eval fanout - """ - return self._fanout class GSgnnLinkPredictionJointTestDataLoader(GSgnnLinkPredictionTestDataLoader): - """ Link prediction minibatch dataloader for validation and test - with joint negative sampler + """ Mini-batch dataloader for Link prediction validation and test set + with joint negative sampler. """ def _prepare_negative_sampler(self, num_negative_edges): @@ -1303,43 +1334,48 @@ def _prepare_negative_sampler(self, num_negative_edges): return negative_sampler class GSgnnLinkPredictionPredefinedTestDataLoader(GSgnnLinkPredictionTestDataLoader): - """ Link prediction minibatch dataloader for validation and test - with predefined negatives. + """ Mini-batch dataloader for link prediction validation and test + with predefined negatives. Parameters ----------- dataset: GSgnnData - The GraphStorm edge dataset + The GraphStorm data. target_idx : dict of Tensors - The target edges for prediction + The target edge indexes for link prediction. batch_size: int - Batch size - fanout: int - Evaluation fanout for computing node embedding + Mini-batch size. + fanout: list of int, or dict of list + Neighbor sampling fanout. If it's a dict of list, it indicates the fanout for each + edge type. fixed_test_size: int Fixed number of test data used in evaluation. If it is none, use the whole testset. - When test is huge, using fixed_test_size + When test is huge, using `fixed_test_size` can save validation and test time. Default: None. - fixed_edge_dst_negative_field: str or list of str - The feature field(s) that store the fixed negative set for each edge. - node_feats: str, or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. - Default: None - edge_feats: str, or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None - pos_graph_edge_feats: str or dist of list of str - The field of the edge features used by positive graph in link prediction. + fixed_edge_dst_negative_field: str, or list of str + The feature fields that store the fixed negative set for each edge. + node_feats: str, or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + + Default: None. + edge_feats: str, or dict of list of str + Edge feature fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. + pos_graph_edge_feats: str, or dict of list of str + The edge feature fields used by positive graph in link prediction. For example edge weight. - Default: None + Default: None. """ def __init__(self, dataset, target_idx, batch_size, fixed_edge_dst_negative_field, fanout=None, fixed_test_size=None, @@ -1378,32 +1414,36 @@ def _next_data(self, etype): class GSgnnNodeDataLoaderBase(): """ The base dataloader class for node tasks. - If users want to customize the dataloader for node prediction tasks + If users want to customize dataloaders for their node prediction tasks, they should extend this base class by implementing the special methods - `__iter__` and `__next__`. + ``__iter__``, ``__next__``, and ``__len__``. Parameters ---------- dataset : GSgnnData - The dataset for the node task. + The GraphStorm data for node tasks. target_idx : dict of Tensors - The target node IDs. - fanout : list or dict of lists + The target node indexes for prediction. + fanout : list of int, or dict of lists The fanout for each GNN layer. - label_field: str or dict of str - Label field of the node task. - node_feats: str, or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. - Default: None - edge_feats: str, or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None + label_field: str, or dict of str + Label field name of the target node types. + node_feats: str, or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + + Default: None. + edge_feats: str, or dict of list of str + Edge feature fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. """ def __init__(self, dataset, target_idx, fanout, label_field, node_feats=None, edge_feats=None): @@ -1418,132 +1458,140 @@ def __init__(self, dataset, target_idx, fanout, self._edge_feats = edge_feats def __iter__(self): - """ Returns an iterator object + """ Returns an iterator object. """ def __next__(self): - """ Return a mini-batch data for the node task. + """ Return a mini-batch data for node tasks. - A mini-batch comprises three objects: the input node IDs of the mini-batch, - the target nodes and the subgraph blocks for message passing. + A mini-batch comprises three objects: 1) the input node IDs of the mini-batch, + 2) the target nodes, and 3) the subgraph blocks for message passing. Returns ------- - dict of Tensors : the input node IDs of the mini-batch. - dict of Tensors : the target node IDs. - list of DGLGraph : the subgraph blocks for message passing. + + - dict of Tensors : the input node IDs of the mini-batch. + - dict of Tensors : the target node indexes. + - list of DGLGraph : the subgraph blocks for message passing. + """ def __len__(self): - """ Return the length (number of mini-batches) of the data loader + """ Return the length (number of mini-batches) of the dataloader. Returns + ------- int: length """ @property def data(self): - """ The dataset of this dataloader. + """ The data of the dataloader, which is given in class initialization. Returns ------- - GSgnnData : The dataset of the dataloader. + GSgnnData : The data of the dataloader. """ return self._data @property def target_nidx(self): - """ Target edge idx for prediction + """ Target edge indexes for prediction , which is given in class initialization. Returns ------- - dict of Tensors : the target edge IDs. + dict of Tensors : the target edge indexes. """ return self._target_idx @property def fanout(self): - """ The fan out of each GNN layers + """ The fan out of each GNN layers , which is given in class initialization. Returns ------- - list or a dict of list : the fanouts for each GNN layer. + list or a dict of list : the fanouts for each GNN layer , which is given in class + initialization. """ return self._fanout @property def label_field(self): - """ The label field + """ The label field, which is given in class initialization. Returns ------- - str: Label fields in the graph. + str, or dict of str: Label fields, which is given in class initialization. """ return self._label_field @property def node_feat_fields(self): - """ Node features + """ Node features fileds, which is given in class initialization. Returns ------- - str or dict of list of str: Node feature fields in the graph. + str, or dict of list of str: Node feature fields, which is given in class initialization. """ return self._node_feats @property def edge_feat_fields(self): - """ Edge features + """ Edge features fields, which is given in class initialization. Returns ------- - str or dict of list of str: Node feature fields in the graph. + str, or dict of list of str: Edge feature fields, which is given in class initialization. """ return self._edge_feats class GSgnnNodeDataLoader(GSgnnNodeDataLoaderBase): - """ Minibatch dataloader for node tasks + """ Mini-batch dataloader for node tasks. - GSgnnNodeDataLoader samples GraphStorm node dataset into an iterable over mini-batches of - samples including target nodes and sampled neighbor nodes, which will be used by GraphStorm + ``GSgnnNodeDataLoader`` samples GraphStorm data into an iterable over mini-batches of + samples, including target nodes and sampled neighbor nodes, which will be used by GraphStorm Trainers and Inferrers. Parameters ---------- dataset: GSgnnData - The GraphStorm dataset + The GraphStorm data. target_idx : dict of Tensors - The target nodes for prediction - fanout: list of int or dict of list - Neighbor sample fanout. If it's a dict, it indicates the fanout for each edge type. + The target node indexes for prediction. + fanout: list of int, or dict of list + Neighbor sampling fanout. If it's a dict of list, it indicates the fanout for each + edge type. label_field: str Label field of the node task. - (TODO:xiangsx) Support list of str for single dataloader multiple node tasks. - node_feats: str, list of str or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. - Default: None - edge_feats: str, list of str or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. - Default: None + node_feats: str, list of str or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + + Default: None. + edge_feats: str, list of str or dict of list of str + Edge feature fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + + Default: None. batch_size: int - Batch size + Mini-batch size. train_task : bool - Whether or not for training. + Whether or not it is the dataloader for training. construct_feat_ntype : list of str The node types that requires to construct node features. construct_feat_fanout : int - The fanout required to construct node features. + The fanout used when constructing node features for feature-less nodes. Examples ---------- To train a 2-layer GNN for node classification on a set of nodes ``target_idx`` on - a graph where each nodes takes messages from 15 neighbors on the first layer + a graph where each node takes messages from 15 neighbors on the first layer and 10 neighbors on the second. .. code:: python @@ -1614,48 +1662,57 @@ def __next__(self): return self.dataloader.__next__() def __len__(self): - # Follow - # https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116 - # In DGL, DistDataLoader.expected_idxs is the length (number of batches) - # of the datalaoder. + """ Follow the + https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116. + In DGL, ``DistDataLoader.expected_idxs`` is the length (number of batches) + of the dataloader. + + Returns: + -------- + int: The length (number of batches) of the dataloader. + """ return self.dataloader.expected_idxs class GSgnnNodeSemiSupDataLoader(GSgnnNodeDataLoader): - """ Semisupervised Minibatch dataloader for node tasks + """ Semi-supervised mini-batch dataloader for node tasks. Parameters ---------- dataset: GSgnnData - The GraphStorm dataset + The GraphStorm data. target_idx : dict of Tensors - The target nodes for prediction + The target node indexes for prediction. unlabeled_idx : dict of Tensors - The unlabeled nodes for semi-supervised training - fanout: list of int or dict of list - Neighbor sample fanout. If it's a dict, it indicates the fanout for each edge type. + The unlabeled node indexes for semi-supervised training. + fanout: list of int, or dict of list + Neighbor sampling fanout. If it's a dict of list, it indicates the fanout for each + edge type. batch_size: int - Batch size, the sum of labeled and unlabeled nodes + Mini-batch size, the sum of labeled and unlabeled nodes label_field: str Label field of the node task. - (TODO:xiangsx) Support list of str for single dataloader multiple node tasks. - node_feats: str, list of str or dist of list of str - Node features. - str: All the nodes have the same feature name. - list of string: All the nodes have the same list of features. - dist of list of string: Each node type have different set of node features. + node_feats: str, list of str, or dict of list of str + Node feature fileds in three possible formats: + + - string: All nodes have the same feature name. + - list of string: All nodes have the same list of features. + - dict of list of string: Each node type have different set of node features. + Default: None - edge_feats: str, list of str or dist of list of str - Edge features. - str: All the edges have the same feature name. - list of string: All the edges have the same list of features. - dist of list of string: Each edge type have different set of edge features. + edge_feats: str, list of str, or dict of list of str + Edge feature fileds in three possible formats: + + - string: All edges have the same feature name. + - list of string: All edges have the same list of features. + - dict of list of string: Each edge type have different set of edge features. + Default: None train_task : bool - Whether or not for training. + Whether or not it is the dataloader for training. construct_feat_ntype : list of str The node types that requires to construct node features. construct_feat_fanout : int - The fanout required to construct node features. + The fanout used when constructing node features for feature-less nodes. """ def __init__(self, dataset, target_idx, unlabeled_idx, fanout, batch_size, label_field, @@ -1683,12 +1740,17 @@ def __next__(self): return self.dataloader.__next__(), self.unlabeled_dataloader.__next__() def __len__(self): - # Follow - # https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116 - # In DGL, DistDataLoader.expected_idxs is the length (number of batches) - # of the datalaoder. - # As it uses two dataloader, either one throws - # an End of Iter error will stop the dataloader. + """ + Follow the + https://github.com/dmlc/dgl/blob/1.0.x/python/dgl/distributed/dist_dataloader.py#L116. + In DGL, ``DistDataLoader.expected_idxs`` is the length (number of batches) + of the dataloader. As it uses two dataloader, either one throws an End of Iter error + will stop the dataloader. + + Returns: + -------- + int: The length (number of batches) of the dataloader. + """ return min(self.dataloader.expected_idxs, self.unlabeled_dataloader.expected_idxs) diff --git a/python/graphstorm/dataloading/dataset.py b/python/graphstorm/dataloading/dataset.py index 2915000a38..bb94e5bcbf 100644 --- a/python/graphstorm/dataloading/dataset.py +++ b/python/graphstorm/dataloading/dataset.py @@ -155,29 +155,28 @@ def prepare_batch_edge_input(g, input_edges, return feat class GSgnnData(): - """ The GraphStorm data + """ The GraphStorm data class. Parameters ---------- part_config : str - The path of the partition configuration file. + The path of the partition configuration JSON file. node_feat_field: str or dict of list of str - The fields of the node features that will be encoded by GSNodeInputLayer. + The fields of the node features that will be encoded by ``GSNodeInputLayer``. It's a dict if different node types have different feature names. - Default: None + Default: None. edge_feat_field : str or dict of list of str - The fields of the edge features. - It's a dict if different edge types have - different feature names. - This argument is reserved by future usage. - Default: None + The fields of the edge features. It's a dict, if different edge types have + different feature names. This argument is reserved for future usage when the + ``GSEdgeInputLayer`` is implemented. + Default: None. lm_feat_ntypes : list of str The node types that contains text features. - Default: None + Default: None. lm_feat_etypes : list of tuples The edge types that contains text features. - Default: None + Default: None. """ def __init__(self, part_config, node_feat_field=None, edge_feat_field=None, @@ -258,24 +257,26 @@ def __init__(self, part_config, node_feat_field=None, edge_feat_field=None, @property def g(self): - """ The distributed graph. + """ The distributed graph loaded using information in the given part_config JSON file. """ return self._g @property def graph_name(self): - """ The graph name + """ The distributed graph's name extracted from the given part_config JSON file. """ return self._graph_name @property def node_feat_field(self): - """The field of node feature""" + """ The fields of node features given in initialization. + """ return self._node_feat_field @property def edge_feat_field(self): - """the field of edge feature""" + """ The fields of edge features given in initialization. + """ return self._edge_feat_field def _check_node_feats(self, node_feat_field): @@ -308,7 +309,7 @@ def has_node_feats(self, ntype): Returns ------- - bool : whether the node type has features. + bool : Whether the node type has features. """ if isinstance(self.node_feat_field, str): return True @@ -323,11 +324,11 @@ def has_edge_feats(self, etype): Parameters ---------- etype : (str, str, str) - The canonical edge type + The canonical edge type. Returns ------- - bool : whether the edge type has features + bool : Whether the edge type has features. """ if isinstance(self.edge_feat_field, str): return True @@ -342,11 +343,11 @@ def has_node_lm_feats(self, ntype): Parameters ---------- ntype : str - The node type + The node type. Returns ------- - bool : whether the node type has features. + bool : Whether the node type has text features. """ return ntype in self._lm_feat_ntypes @@ -356,23 +357,24 @@ def has_edge_lm_feats(self, etype): Parameters ---------- etype : (str, str, str) - The edge type + The edge type. Returns ------- - bool : whether the node type has features. + bool : Whether the edge type has text features. """ return etype in self._lm_feat_etypes def get_node_feats(self, input_nodes, nfeat_fields, device='cpu'): - """ Get the node features + """ Get the node features of the given input nodes. The feature fields are defined + in ``nfeat_fields``. Parameters ---------- input_nodes : Tensor or dict of Tensors - The input node IDs - nfeat_fields : str or dict of list - The node features to collect from graph + The input node IDs. + nfeat_fields : str or dict of [str ...] + The node feature fields to be extracted. device : Pytorch device The device where the returned node features are stored. @@ -390,14 +392,15 @@ def get_node_feats(self, input_nodes, nfeat_fields, device='cpu'): feat_field=nfeat_fields) def get_edge_feats(self, input_edges, efeat_fields, device='cpu'): - """ Get the edge features + """ Get the edge features of the given input edges. The feature fields are defined + in ``efeat_fields``. Parameters ---------- input_edges : Tensor or dict of Tensors - The input edge IDs + The input edge IDs. efeat_fields: str or dict of [str ..] - The edge data fields that stores the edge features to retrieve + The edge feature fields to be extracted. device : Pytorch device The device where the returned edge features are stored. @@ -473,15 +476,15 @@ def _check_node_mask(self, ntypes, masks): return masks def get_unlabeled_node_set(self, train_idxs, mask="train_mask"): - """ Collect nodes not used for training. + """ Get node indexes not having the given mask in the training set. Parameters __________ - train_idxs: dict + train_idxs: dict of Tensor The training set. mask: str or list of str - The node feature field storing the training mask. - Default: "train_mask" + The node feature fields storing the training mask. + Default: "train_mask". Returns ------- @@ -510,19 +513,19 @@ def get_unlabeled_node_set(self, train_idxs, mask="train_mask"): return unlabeled_idxs def get_node_train_set(self, ntypes, mask="train_mask"): - """ Get node training set for nodes of ntypes. + """ Get the training set for the given node types under the given mask. Parameters __________ ntypes: str or list of str Node types to get the training set. mask: str or list of str - The node feature field storing the training mask. - Default: "train_mask" + The node feature fields storing the training mask. + Default: "train_mask". Returns ------- - dict of Tensors : The returned training node indexes + dict of Tensors : The returned training node indexes. """ g = self._g pb = g.get_partition_book() @@ -584,19 +587,19 @@ def _get_node_set(self, ntypes, mask): return idxs, num_data def get_node_val_set(self, ntypes, mask="val_mask"): - """ Get node validation set for nodes of ntypes. + """ Get the validation set for the given node types under the given mask. Parameters __________ ntypes: str or list of str Node types to get the validation set. mask: str or list of str - The node feature field storing the validation mask. - Default: "val_mask" + The node feature fields storing the validation mask. + Default: "val_mask". Returns ------- - dict of Tensors : The returned validation node indexes + dict of Tensors : The returned validation node indexes. """ idxs, num_data = self._get_node_set(ntypes, mask) logging.info('part %d, val %d', get_rank(), num_data) @@ -604,19 +607,19 @@ def get_node_val_set(self, ntypes, mask="val_mask"): return idxs def get_node_test_set(self, ntypes, mask="test_mask"): - """ Get node test set for nodes of ntypes. + """ Get the test set for the given node types under the given mask. Parameters __________ ntypes: str or list of str Node types to get the test set. mask: str or list of str - The node feature field storing the test mask. - Default: "test_mask" + The node feature fields storing the test mask. + Default: "test_mask". Returns ------- - dict of Tensors : The returned test node indexes + dict of Tensors : The returned test node indexes. """ idxs, num_data = self._get_node_set(ntypes, mask) logging.info('part %d, test %d', get_rank(), num_data) @@ -624,19 +627,19 @@ def get_node_test_set(self, ntypes, mask="test_mask"): return idxs def get_node_infer_set(self, ntypes, mask="test_mask"): - """ Get node set for inference. + """ Get inference node set for the given node types under the given mask. - If the mask exists in g.nodes[ntype].data, the inference set + If the mask exists in ``g.nodes[ntype].data``, the inference set is collected based on the mask. - If not, the entire node set are treated as the inference set. + If not exist, the entire node set are treated as the inference set. Parameters __________ ntypes: str or list of str Node types to get the inference set. mask: str or list of str - The node feature field storing the inference mask. - Default: "test_mask" + The node feature fields storing the inference mask. + Default: "test_mask". Returns ------- @@ -738,20 +741,20 @@ def _exclude_reverse_etype(self, etypes, reverse_edge_types_map=None): def get_edge_train_set(self, etypes=None, mask="train_mask", reverse_edge_types_map=None): - """ Get edge training set for edges of etypes. + """ Get the training set for the given edge types under the given mask. Parameters __________ etypes: list of str List of edge types to get the training set. If set to None, all the edge types are included. - Default: None + Default: None. mask: str or list of str - The edge feature field storing the training mask. - Default: "train_mask" - reverse_edge_types_map: dict - A map for reverse edge type. - Default: None + The edge feature fields storing the training mask. + Default: "train_mask". + reverse_edge_types_map: dict of tupeles + A map for reverse edge types in the format of {(edge type):(reversed edge type)}. + Default: None. Returns ------- @@ -820,7 +823,7 @@ def _get_edge_set(self, etypes, mask, reverse_edge_types_map): def get_edge_val_set(self, etypes=None, mask="val_mask", reverse_edge_types_map=None): - """ Get edge validation set for edges of etypes. + """ Get the validation set for the given edge types under the given mask. Parameters __________ @@ -829,13 +832,14 @@ def get_edge_val_set(self, etypes=None, mask="val_mask", If set to None, all the edge types are included. mask: str or list of str The edge feature field storing the val mask. - Default: "val_mask" + Default: "val_mask". reverse_edge_types_map: dict - A map for reverse edge type. + A map for reverse edge types in the format of {(edge type):(reversed edge type)}. + Default: None. Returns ------- - dict of Tensors : The returned validation edge indexes + dict of Tensors : The returned validation edge indexes. """ idxs, num_data = self._get_edge_set(etypes, mask, reverse_edge_types_map) logging.info('part %d, val %d', get_rank(), num_data) @@ -844,7 +848,7 @@ def get_edge_val_set(self, etypes=None, mask="val_mask", def get_edge_test_set(self, etypes=None, mask="test_mask", reverse_edge_types_map=None): - """ Get edge test set for edges of etypes. + """ Get the test set for the given edge types under the given mask. Parameters __________ @@ -853,9 +857,10 @@ def get_edge_test_set(self, etypes=None, mask="test_mask", If set to None, all the edge types are included. mask: str or list of str The edge feature field storing the test mask. - Default: "test_mask" + Default: "test_mask". reverse_edge_types_map: dict - A map for reverse edge type. + A map for reverse edge types in the format of {(edge type):(reversed edge type)}. + Default: None. Returns ------- @@ -867,27 +872,28 @@ def get_edge_test_set(self, etypes=None, mask="test_mask", return idxs def get_edge_infer_set(self, etypes=None, mask="test_mask", reverse_edge_types_map=None): - """ Get edge set for inference. + """ Get the inference set for the given edge types under the given mask. - If the mask exists in g.edges[etype].data, the inference set + If the mask exists in ``g.edges[etype].data``, the inference set is collected based on the mask. - If not, the entire edge set are treated as the inference set. + If not exist, the entire edge set are treated as the inference set. Parameters __________ etypes: list of str List of edge types to get the inference set. If set to None, all the edge types are included. - Default: None + Default: None. mask: str or list of str The edge feature field storing the inference mask. - Default: "test_mask" + Default: "test_mask". reverse_edge_types_map: dict - A map for reverse edge type. + A map for reverse edge types in the format of {(edge type):(reversed edge type)}. + Default: None. Returns ------- - dict of Tensors : The returned inference edge indexes + dict of Tensors : The returned inference edge indexes. """ g = self._g pb = g.get_partition_book() diff --git a/python/graphstorm/trainer/lp_trainer.py b/python/graphstorm/trainer/lp_trainer.py index 5308a75204..182e5feecd 100644 --- a/python/graphstorm/trainer/lp_trainer.py +++ b/python/graphstorm/trainer/lp_trainer.py @@ -182,11 +182,11 @@ def fit(self, train_loader, num_epochs, input_nodes = {pos_graph.ntypes[0]: input_nodes} nfeat_fields = train_loader.node_feat_fields input_feats = data.get_node_feats(input_nodes, nfeat_fields, device) - if train_loader.pos_graph_feat_fields is not None: + if train_loader.pos_graph_edge_feat_fields is not None: input_edges = {etype: pos_graph.edges[etype].data[dgl.EID] \ for etype in pos_graph.canonical_etypes} pos_graph_feats = data.get_edge_feats(input_edges, - train_loader.pos_graph_feat_fields, + train_loader.pos_graph_edge_feat_fields, device) else: pos_graph_feats = None diff --git a/python/graphstorm/trainer/mt_trainer.py b/python/graphstorm/trainer/mt_trainer.py index 630e70235d..a9e13ba0f8 100644 --- a/python/graphstorm/trainer/mt_trainer.py +++ b/python/graphstorm/trainer/mt_trainer.py @@ -180,11 +180,11 @@ def prepare_link_predict_mini_batch(data, task_info, mini_batch, device): nfeat_fields = task_info.dataloader.node_feat_fields node_feats = data.get_node_feats(input_nodes, nfeat_fields, device) - if task_info.dataloader.pos_graph_feat_fields is not None: + if task_info.dataloader.pos_graph_edge_feat_fields is not None: input_edges = {etype: pos_graph.edges[etype].data[dgl.EID] \ for etype in pos_graph.canonical_etypes} pos_graph_feats = data.get_edge_feats(input_edges, - task_info.dataloader.pos_graph_feat_fields, + task_info.dataloader.pos_graph_edge_feat_fields, device) else: pos_graph_feats = None From 1d1b21fdf96d1ee76fb6f31699b3afdac9e32b88 Mon Sep 17 00:00:00 2001 From: "xiang song(charlie.song)" Date: Mon, 29 Jul 2024 18:52:48 -0700 Subject: [PATCH 3/4] [BugFix] Fix missing node normalization for link prediction tasks in multi-task learning (#926) *Issue #, if available:* In multitask learning, when there is a training link prediction task with contrastive loss, the loss may become NaN. This is because, GraphStorm does not add proper node normalization for the gnn embeddings. *Description of changes:* Fix the bug. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Xiang Song --- .../ml_nc_lp_norm_with_mask_infer.yaml | 59 ++++++++ python/graphstorm/gconstruct/remap_result.py | 65 ++++++++- python/graphstorm/inference/mt_infer.py | 41 ++++-- python/graphstorm/model/multitask_gnn.py | 129 +++++++++++++++++- python/graphstorm/run/gsgnn_mt/gsgnn_mt.py | 37 ++++- .../graphstorm/run/gsgnn_mt/mt_infer_gnn.py | 12 +- python/graphstorm/trainer/mt_trainer.py | 6 + .../end2end-tests/graphstorm-mt/mgpu_test.sh | 47 ++++++- .../gconstruct/test_remap_result.py | 25 +++- tests/unit-tests/test_gnn.py | 125 ++++++++++++++++- tests/unit-tests/util.py | 6 + training_scripts/gsgnn_mt/ml_nc_lp_norm.yaml | 66 +++++++++ 12 files changed, 581 insertions(+), 37 deletions(-) create mode 100644 inference_scripts/mt_infer/ml_nc_lp_norm_with_mask_infer.yaml create mode 100644 training_scripts/gsgnn_mt/ml_nc_lp_norm.yaml diff --git a/inference_scripts/mt_infer/ml_nc_lp_norm_with_mask_infer.yaml b/inference_scripts/mt_infer/ml_nc_lp_norm_with_mask_infer.yaml new file mode 100644 index 0000000000..53e2d267dc --- /dev/null +++ b/inference_scripts/mt_infer/ml_nc_lp_norm_with_mask_infer.yaml @@ -0,0 +1,59 @@ +--- +version: 1.0 +gsf: + basic: + backend: gloo + verbose: false + save_perf_results_path: null + batch_size: 32 + node_feat_name: + - user:feat + - movie:title + gnn: + model_encoder_type: rgcn + num_layers: 1 + hidden_size: 32 + use_mini_batch_infer: true + input: + restore_model_path: null + output: + save_model_path: null + save_embed_path: null + hyperparam: + dropout: 0. + lr: 0.001 + no_validation: false + rgcn: + num_bases: -1 + use_self_loop: true + use_node_embeddings: false + multi_task_learning: + - node_classification: + target_ntype: "movie" + label_field: "label" + multilabel: false + num_classes: 19 + batch_size: 16 # will overwrite the global batch_size + mask_fields: + - "train_mask_c0" # node classification mask 0 + - "val_mask_c0" + - "test_mask_c0" + eval_metric: + - "accuracy" + - link_prediction: + lp_loss_func: "contrastive" + num_negative_edges: 4 + num_negative_edges_eval: 100 + train_negative_sampler: joint + eval_etype: + - "user,rating,movie" + train_etype: + - "user,rating,movie" + exclude_training_targets: true + reverse_edge_types_map: + - user,rating,rating-rev,movie + batch_size: 128 # will overwrite the global batch_size + mask_fields: + - "train_mask_field_lp" + - null # empty means there is no validation mask + - "test_mask_field_lp" \ No newline at end of file diff --git a/python/graphstorm/gconstruct/remap_result.py b/python/graphstorm/gconstruct/remap_result.py index d88219e1d5..e19eebf7d6 100644 --- a/python/graphstorm/gconstruct/remap_result.py +++ b/python/graphstorm/gconstruct/remap_result.py @@ -40,7 +40,8 @@ BUILTIN_TASK_EDGE_CLASSIFICATION, BUILTIN_TASK_EDGE_REGRESSION, BUILTIN_TASK_NODE_CLASSIFICATION, - BUILTIN_TASK_NODE_REGRESSION) + BUILTIN_TASK_NODE_REGRESSION, + BUILTIN_TASK_LINK_PREDICTION) GS_OUTPUT_FORMAT_PARQUET = "parquet" GS_OUTPUT_FORMAT_CSV = "csv" @@ -655,16 +656,28 @@ def _parse_gs_config(config): node_id_mapping = os.path.join(os.path.dirname(part_config), "raw_id_mappings") predict_dir = config.save_prediction_path emb_dir = config.save_embed_path + task_emb_dirs = [] pred_ntypes = [] pred_etypes = [] if config.multi_tasks is not None: node_predict_dirs = [] edge_predict_dirs = [] - if predict_dir is None: - return node_id_mapping, None, emb_dir, pred_ntypes, pred_etypes # multi-task setting tasks = config.multi_tasks + + for task in tasks: + task_config = task.task_config + task_id = task.task_id + if task.task_type in [BUILTIN_TASK_LINK_PREDICTION]: + if task_config.lp_embed_normalizer is not None: + # There are link prediction node embedding normalizer + # Need to handled the normalized embeddings. + task_emb_dirs.append(task_id) + + if predict_dir is None: + return node_id_mapping, None, emb_dir, task_emb_dirs, pred_ntypes, pred_etypes + for task in tasks: task_config = task.task_config task_id = task.task_id @@ -681,7 +694,7 @@ def _parse_gs_config(config): edge_predict_dirs.append(pred_path) predict_dir = (node_predict_dirs, edge_predict_dirs) - return node_id_mapping, predict_dir, emb_dir, pred_ntypes, pred_etypes + return node_id_mapping, predict_dir, emb_dir, task_emb_dirs, pred_ntypes, pred_etypes else: task_type = config.task_type if task_type in (BUILTIN_TASK_EDGE_CLASSIFICATION, BUILTIN_TASK_EDGE_REGRESSION): @@ -694,7 +707,7 @@ def _parse_gs_config(config): pred_ntypes = pred_ntypes \ if isinstance(pred_ntypes, list) else [pred_ntypes] - return node_id_mapping, predict_dir, emb_dir, pred_ntypes, pred_etypes + return node_id_mapping, predict_dir, emb_dir, task_emb_dirs, pred_ntypes, pred_etypes def main(args, gs_config_args): """ main function @@ -714,7 +727,7 @@ def main(args, gs_config_args): gs_args, _ = gs_parser.parse_known_args(gs_config_args) config = GSConfig(gs_args) config.verify_arguments(False) - id_mapping_path, predict_dir, node_emb_dir, pred_ntypes, pred_etypes = \ + id_mapping_path, predict_dir, node_emb_dir, task_emb_dirs, pred_ntypes, pred_etypes = \ _parse_gs_config(config) else: # Case 2: remap_result is called alone. @@ -724,6 +737,10 @@ def main(args, gs_config_args): id_mapping_path = args.node_id_mapping predict_dir = args.prediction_dir node_emb_dir = args.node_emb_dir + # We do not handle the case when there are task specific embeddings + # in multi-task learning, if remap_result is called alone. + # Users need to clean up the node_emb_dir themselves. + task_emb_dirs = [] pred_etypes = args.pred_etypes pred_ntypes = args.pred_ntypes if pred_etypes is not None: @@ -773,7 +790,26 @@ def main(args, gs_config_args): else: # There is no shared file system emb_names = os.listdir(node_emb_dir) - emb_names = [e_name for e_name in emb_names if e_name != "emb_info.json"] + # In single task learning, the node embed dir looks like: + # emb_dir/ + # ntype0 + # ntype1 + # ... + # emb_info.json + # + # In multi-task learning, the node embed dir looks like: + # emb_dir/ + # ntype0 + # ntype1 + # ... + # emb_info.json + # task_id0/ + # task_id1/ + # ... + # We need to exclude both emb_info.json and task_id directories, + # when we are collecting node types with node embeddings. + emb_names = [e_name for e_name in emb_names \ + if e_name not in task_emb_dirs + ["emb_info.json"]] emb_ntypes = emb_names else: @@ -962,6 +998,21 @@ def main(args, gs_config_args): output_func) files_to_remove += emb_files_to_remove + for task_emb_dir in task_emb_dirs: + task_emb_dir = os.path.join(node_emb_dir, task_emb_dir) + # We need to do ID remapping for node embeddings + emb_files_to_remove = \ + remap_node_emb(emb_ntypes, + task_emb_dir, + task_emb_dir, + out_chunk_size, + num_proc, + rank, + world_size, + with_shared_fs, + output_func) + files_to_remove += emb_files_to_remove + if len(pred_etypes) > 0: if isinstance(predict_dir, tuple): _, edge_predict_dirs = predict_dir diff --git a/python/graphstorm/inference/mt_infer.py b/python/graphstorm/inference/mt_infer.py index 142c5636a4..05943ccb9b 100644 --- a/python/graphstorm/inference/mt_infer.py +++ b/python/graphstorm/inference/mt_infer.py @@ -105,7 +105,8 @@ def infer(self, data, """ do_eval = self.evaluator is not None sys_tracker.check('start inferencing') - self._model.eval() + model = self._model + model.eval() # All the tasks share the same GNN encoder so the fanouts are same # for different tasks. @@ -133,13 +134,13 @@ def gen_embs(edge_mask=None): # so the node embeddings are updated inplace. if use_mini_batch_infer: embs = do_mini_batch_inference( - self._model, data, batch_size=infer_batch_size, + model, data, batch_size=infer_batch_size, fanout=fanout, edge_mask=edge_mask, task_tracker=self.task_tracker) else: embs = do_full_graph_inference( - self._model, data, + model, data, fanout=fanout, edge_mask=edge_mask, task_tracker=self.task_tracker) @@ -154,17 +155,29 @@ def gen_embs(edge_mask=None): # before conducting prediction results. if save_embed_path is not None: logging.info("Saving node embeddings") + node_norm_methods = model.node_embed_norm_methods + # Save the original embs first save_gsgnn_embeddings(g, save_embed_path, embs, node_id_mapping_file=node_id_mapping_file, save_embed_format=save_embed_format) barrier() + for task_id, norm_method in node_norm_methods.items(): + if norm_method is None: + continue + normed_embs = model.normalize_task_node_embs(task_id, embs, inplace=False) + save_embed_path = os.path.join(save_embed_path, task_id) + save_gsgnn_embeddings(g, + save_embed_path, + normed_embs, + node_id_mapping_file=node_id_mapping_file, + save_embed_format=save_embed_format) sys_tracker.check('save embeddings') # save relation embedding if any for link prediction tasks if get_rank() == 0: - decoders = self._model.task_decoders + decoders = model.task_decoders for task_id, decoder in decoders.items(): if isinstance(decoder, LinkPredictDistMultDecoder): rel_emb_path = os.path.join(save_embed_path, task_id) @@ -189,7 +202,7 @@ def gen_embs(edge_mask=None): # and edge regression tasks. pre_results = \ multi_task_mini_batch_predict( - self._model, + model, emb=embs, dataloaders=predict_test_loader.dataloaders, task_infos=predict_test_loader.task_infos, @@ -213,9 +226,9 @@ def nfrecon_gen_embs(skip_last_self_loop=False, node_embs=embs): if skip_last_self_loop is True: # Turn off the last layer GNN's self-loop # to compute node embeddings. - self._model.gnn_encoder.skip_last_selfloop() + model.gnn_encoder.skip_last_selfloop() new_embs = gen_embs() - self._model.gnn_encoder.reset_last_selfloop() + model.gnn_encoder.reset_last_selfloop() return new_embs else: # If skip_last_self_loop is False @@ -231,11 +244,11 @@ def nfrecon_gen_embs(skip_last_self_loop=False, node_embs=embs): # Note(xiangsx): In DistDGl, as we are using the # same dist tensor, the node embeddings # are updated inplace. - nfeat_embs = gen_emb_for_nfeat_reconstruct(self._model, nfrecon_gen_embs) + nfeat_embs = gen_emb_for_nfeat_reconstruct(model, nfrecon_gen_embs) nfeat_recon_results = \ multi_task_mini_batch_predict( - self._model, + model, emb=nfeat_embs, dataloaders=dataloaders, task_infos=task_infos, @@ -258,8 +271,14 @@ def nfrecon_gen_embs(skip_last_self_loop=False, node_embs=embs): # For link prediction, do evaluation task by task. lp_test_embs = gen_embs(edge_mask=task_info.task_config.train_mask) - - decoder = self._model.task_decoders[task_info.task_id] + # normalize the node embedding if needed. + # we can do inplace normalization as embeddings are generated + # per lp task. + lp_test_embs = model.normalize_task_node_embs(task_info.task_id, + lp_test_embs, + inplace=True) + + decoder = model.task_decoders[task_info.task_id] ranking = run_lp_mini_batch_predict(decoder, lp_test_embs, dataloader, device) pre_results[task_info.task_id] = ranking diff --git a/python/graphstorm/model/multitask_gnn.py b/python/graphstorm/model/multitask_gnn.py index 02a679eb70..f5b964e33e 100644 --- a/python/graphstorm/model/multitask_gnn.py +++ b/python/graphstorm/model/multitask_gnn.py @@ -19,6 +19,7 @@ import logging import torch as th from torch import nn +import dgl from ..config import (BUILTIN_TASK_NODE_CLASSIFICATION, BUILTIN_TASK_NODE_REGRESSION, @@ -32,7 +33,14 @@ from .node_gnn import run_node_mini_batch_predict from .edge_gnn import run_edge_mini_batch_predict from .lp_gnn import run_lp_mini_batch_predict - +from .utils import LazyDistTensor +from .utils import normalize_node_embs, get_data_range +from ..utils import ( + get_rank, + get_world_size, + barrier, + create_dist_tensor +) class GSgnnMultiTaskModelInterface: """ The interface for GraphStorm multi-task learning. @@ -93,10 +101,108 @@ def __init__(self, alpha_l2norm): self._task_pool = {} self._decoder = nn.ModuleDict() self._loss_fn = nn.ModuleDict() + self._node_embed_norm_methods = {} self._warn_printed = False + def normalize_task_node_embs(self, task_id, embs, inplace=False): + """ Normalize node embeddings when needed. + + normalize_task_node_embs should be called when embs stores embeddings + of every node. + + Parameters + ---------- + task_id: str + Task ID. + embs: dict of Tensors + A dict of node embeddings. + inplace: bool + Whether to do inplace normalization. + + Returns + ------- + new_embs: dict of Tensors + Normalized node embeddings. + """ + if self._node_embed_norm_methods[task_id] is not None: + new_embs = {} + rank = get_rank() + world_size = get_world_size() + for key, emb in embs.items(): + if isinstance(emb, (dgl.distributed.DistTensor, LazyDistTensor)): + # If emb is a distributed tensor, multiple processes are doing + # embdding normalization concurrently. We need to split + # the task. (From full_graph_inference) + start, end = get_data_range(rank, world_size, len(emb)) + new_emb = emb if inplace else \ + create_dist_tensor(emb.shape, + emb.dtype, + name=f"{emb.name}_task_id", + part_policy=emb.part_policy, + persistent=True) + else: + # If emb is just a torch Tensor. do normalization directly. + # (From mini_batch_inference) + start, end = 0, len(emb) + new_emb = emb if inplace else th.clone(emb) + idx = start + while idx + 1024 < end: + new_emb[idx:idx+1024] = \ + self.minibatch_normalize_task_node_embs( + task_id, + {key:emb[idx:idx+1024]})[key] + idx += 1024 + new_emb[idx:end] = \ + self.minibatch_normalize_task_node_embs( + task_id, + {key:emb[idx:end]})[key] + barrier() + new_embs[key] = new_emb + return new_embs + else: + # If normalization method is None + # do nothing. + new_embs = embs + return new_embs + + # pylint: disable = arguments-differ + def minibatch_normalize_task_node_embs(self, task_id, embs): + """ Normalize node embeddings when needed for a mini-batch. + + minibatch_normalize_task_node_embs should be called in + forward() and predict(). + + Parameters + ---------- + task_id: str + Task ID. + embs: dict of Tensors + A dict of node embeddings. + + Returns + ------- + embs: dict of Tensors + Normalized node embeddings. + """ + if self._node_embed_norm_methods[task_id] is not None: + return normalize_node_embs(embs, self._node_embed_norm_methods[task_id]) + else: + return embs + + @property + def node_embed_norm_methods(self): + """ Get per task node embedding normalization method + + Returns + ------- + dict of strings: + Normalization methods + """ + return self._node_embed_norm_methods + def add_task(self, task_id, task_type, - decoder, loss_func): + decoder, loss_func, + embed_norm_method=None): """ Add a task into the multi-task pool Parameters @@ -112,6 +218,8 @@ def add_task(self, task_id, task_type, Task decoder. loss_func: func Loss function. + embed_norm_method: str + Node embedding normalization method. """ assert task_id not in self._task_pool, \ f"Task {task_id} already exists" @@ -120,6 +228,7 @@ def add_task(self, task_id, task_type, self._decoder[task_id] = decoder # add loss func in nn module self._loss_fn[task_id] = loss_func + self._node_embed_norm_methods[task_id] = embed_norm_method @property def alpha_l2norm(self): @@ -277,7 +386,7 @@ def _forward(self, task_id, encoder_data, decoder_data): encode_embs = self.compute_embed_step(blocks, node_feats, input_nodes) # Call emb normalization. - encode_embs = self.normalize_node_embs(encode_embs) + encode_embs = self.minibatch_normalize_task_node_embs(task_id, encode_embs) if task_type in [BUILTIN_TASK_NODE_CLASSIFICATION, BUILTIN_TASK_NODE_REGRESSION]: labels = decoder_data @@ -353,7 +462,7 @@ def predict(self, task_id, mini_batch, return_proba=False): encode_embs = self.compute_embed_step(blocks, node_feats, input_nodes) # Call emb normalization. - encode_embs = self.normalize_node_embs(encode_embs) + encode_embs = self.minibatch_normalize_task_node_embs(task_id, encode_embs) task_type, _ = self.task_pool[task_id] task_decoder = self.decoder[task_id] @@ -415,6 +524,18 @@ def multi_task_mini_batch_predict( res = {} with th.no_grad(): for dataloader, task_info in zip(dataloaders, task_infos): + # normalize the node embedding if needed. + # input emb is shared across different tasks + # so that we can not do inplace normalization. + # + # Note(xiangsx): Currently node embedding normalization + # only supports link prediction tasks. + # model.normalize_task_node_embs does nothing + # for node and edge prediction tasks. + # TODO(xiangsx): Need a more memory efficient design when + # node embedding normalization supports node and edge + # prediction tasks. + emb = model.normalize_task_node_embs(task_info.task_id, emb, inplace=False) if task_info.task_type in \ [BUILTIN_TASK_NODE_CLASSIFICATION, BUILTIN_TASK_NODE_REGRESSION, diff --git a/python/graphstorm/run/gsgnn_mt/gsgnn_mt.py b/python/graphstorm/run/gsgnn_mt/gsgnn_mt.py index 19d63b77b0..304fabbe16 100644 --- a/python/graphstorm/run/gsgnn_mt/gsgnn_mt.py +++ b/python/graphstorm/run/gsgnn_mt/gsgnn_mt.py @@ -348,7 +348,17 @@ def main(config_args): train_data.g, encoder_out_dims, train_task=True) - model.add_task(task.task_id, task.task_type, decoder, loss_func) + # For link prediction, lp_embed_normalizer may be used + # TODO(xiangsx): add embed normalizer for other task types + # in the future. + node_embed_norm_method = task.task_config.lp_embed_normalizer \ + if task.task_type in [BUILTIN_TASK_LINK_PREDICTION] \ + else None + model.add_task(task.task_id, + task.task_type, + decoder, + loss_func, + embed_norm_method=node_embed_norm_method) if not config.no_validation: if val_loader is None: logging.warning("The training data do not have validation set.") @@ -419,7 +429,14 @@ def main(config_args): train_data.g, encoder_out_dims, train_task=True) - model.add_task(task.task_id, task.task_type, decoder, loss_func) + node_embed_norm_method = task.task_config.lp_embed_normalizer \ + if task.task_type in [BUILTIN_TASK_LINK_PREDICTION] \ + else None + model.add_task(task.task_id, + task.task_type, + decoder, + loss_func, + embed_norm_method=node_embed_norm_method) best_model_path = trainer.get_best_model_path() # TODO(zhengda) the model path has to be in a shared filesystem. model.restore_model(best_model_path) @@ -432,6 +449,7 @@ def main(config_args): embeddings = do_full_graph_inference(model, train_data, fanout=config.eval_fanout, task_tracker=tracker) + # Save the original embs first save_full_node_embeddings( train_data.g, config.save_embed_path, @@ -439,6 +457,21 @@ def main(config_args): node_id_mapping_file=config.node_id_mapping_file, save_embed_format=config.save_embed_format) + node_norm_methods = model.node_embed_norm_methods + # save normalized embeddings + for task_id, norm_method in node_norm_methods.items(): + if norm_method is None: + continue + normed_embs = model.normalize_task_node_embs(task_id, embeddings, inplace=False) + save_embed_path = os.path.join(config.save_embed_path, task_id) + save_full_node_embeddings( + train_data.g, + save_embed_path, + normed_embs, + node_id_mapping_file=config.node_id_mapping_file, + save_embed_format=config.save_embed_format) + + def generate_parser(): """ Generate an argument parser """ diff --git a/python/graphstorm/run/gsgnn_mt/mt_infer_gnn.py b/python/graphstorm/run/gsgnn_mt/mt_infer_gnn.py index 718625f3f1..6c4122004b 100644 --- a/python/graphstorm/run/gsgnn_mt/mt_infer_gnn.py +++ b/python/graphstorm/run/gsgnn_mt/mt_infer_gnn.py @@ -218,7 +218,17 @@ def main(config_args): predict_dataloaders.append(data_loader) predict_tasks.append(task) - model.add_task(task.task_id, task.task_type, decoder, loss_func) + # For link prediction, lp_embed_normalizer may be used + # TODO(xiangsx): add embed normalizer for other task types + # in the future. + node_embed_norm_method = task.task_config.lp_embed_normalizer \ + if task.task_type in [BUILTIN_TASK_LINK_PREDICTION] \ + else None + model.add_task(task.task_id, + task.task_type, + decoder, + loss_func, + embed_norm_method=node_embed_norm_method) # Multi-task testing dataloader for node prediction and # edge prediction tasks. diff --git a/python/graphstorm/trainer/mt_trainer.py b/python/graphstorm/trainer/mt_trainer.py index a9e13ba0f8..2f2787baaa 100644 --- a/python/graphstorm/trainer/mt_trainer.py +++ b/python/graphstorm/trainer/mt_trainer.py @@ -641,6 +641,12 @@ def gen_embs(edge_mask=None): # For link prediction, do evaluation task # by task. lp_test_embs = gen_embs(edge_mask=task_info.task_config.train_mask) + # normalize the node embedding if needed. + # we can do inplace normalization as embeddings are generated + # per lp task. + lp_test_embs = model.normalize_task_node_embs(task_info.task_id, + lp_test_embs, + inplace=True) decoder = model.task_decoders[task_info.task_id] val_scores = run_lp_mini_batch_predict(decoder, diff --git a/tests/end2end-tests/graphstorm-mt/mgpu_test.sh b/tests/end2end-tests/graphstorm-mt/mgpu_test.sh index aceb326ac6..67eb3211c8 100644 --- a/tests/end2end-tests/graphstorm-mt/mgpu_test.sh +++ b/tests/end2end-tests/graphstorm-mt/mgpu_test.sh @@ -674,6 +674,49 @@ python3 $GS_HOME/tests/end2end-tests/check_infer.py --train-embout /data/gsgnn_m error_and_exit $? -rm -fr /data/gsgnn_mt/infer-emb/ -rm -fr /data/gsgnn_mt/prediction/ +rm -fr /data/gsgnn_mt/ rm -fr /tmp/infer_log.txt + + +echo "**************[Multi-task] dataset: Movielens, RGCN layer 1, node feat: fixed HF BERT, BERT nodes: movie, inference: full-graph, save model" +python3 -m graphstorm.run.gs_multi_task_learning --workspace $GS_HOME/training_scripts/gsgnn_mt --num-trainers $NUM_TRAINERS --num-servers 1 --num-samplers 0 --part-config /data/movielen_100k_multi_task_train_val_1p_4t/movie-lens-100k.json --ip-config ip_list.txt --ssh-port 2222 --cf ml_nc_lp_norm.yaml --save-model-path /data/gsgnn_mt/ --save-model-frequency 1000 --logging-file /tmp/train_log.txt --logging-level debug --preserve-input True --use-mini-batch-infer False --save-embed-path /data/gsgnn_mt/emb/ + +error_and_exit $? + +cnt=$(grep "save_embed_path: /data/gsgnn_mt/emb/" /tmp/train_log.txt | wc -l) +if test $cnt -lt 1 +then + echo "We use SageMaker task tracker, we should have save_embed_path" + exit -1 +fi + +cnt=$(ls -l /data/gsgnn_mt/emb/ | wc -l) +cnt=$[cnt - 1] +if test $cnt != 3 +then + echo "The number of saved embs $cnt is not equal to 3. Should have two for movie and user and One for link-prediction-subtask normalized embedding." +fi + +echo "**************[Multi-task] dataset: Movielens, RGCN layer 1, node feat: fixed HF BERT, BERT nodes: movie, inference with test" +python3 -m graphstorm.run.gs_multi_task_learning --inference --workspace $GS_HOME/inference_scripts/mt_infer --num-trainers $NUM_INFERs --num-servers 1 --num-samplers 0 --part-config /data/movielen_100k_multi_task_train_val_1p_4t/movie-lens-100k.json --ip-config ip_list.txt --ssh-port 2222 --cf ml_nc_lp_norm_with_mask_infer.yaml --use-mini-batch-infer false --save-embed-path /data/gsgnn_mt/infer-emb/ --restore-model-path /data/gsgnn_mt/epoch-2 --save-prediction-path /data/gsgnn_mt/prediction/ --logging-file /tmp/infer_log.txt --preserve-input True + +error_and_exit $? + +cnt=$(ls -l /data/gsgnn_mt/infer-emb/ | wc -l) +cnt=$[cnt - 2] +if test $cnt != 4 +then + echo "The number of saved embs $cnt is not equal to 3. Should have two for movie and user and One for link-prediction-subtask normalized embedding." +fi + +python3 $GS_HOME/tests/end2end-tests/check_infer.py --train-embout /data/gsgnn_mt/emb/ --infer-embout /data/gsgnn_mt/infer-emb/ + +error_and_exit $? + +python3 $GS_HOME/tests/end2end-tests/check_infer.py --train-embout /data/gsgnn_mt/emb/link_prediction-user_rating_movie --infer-embout /data/gsgnn_mt/infer-emb/link_prediction-user_rating_movie + +error_and_exit $? + +rm -fr /data/gsgnn_mt/ +rm -fr /tmp/train_log.txt +rm -fr /tmp/infer_log.txt \ No newline at end of file diff --git a/tests/unit-tests/gconstruct/test_remap_result.py b/tests/unit-tests/gconstruct/test_remap_result.py index 7ac1c7156e..ca48a81586 100644 --- a/tests/unit-tests/gconstruct/test_remap_result.py +++ b/tests/unit-tests/gconstruct/test_remap_result.py @@ -26,6 +26,7 @@ from numpy.testing import assert_equal, assert_almost_equal from graphstorm.config import GSConfig +from graphstorm.config.config import get_mttask_id from graphstorm.config import (BUILTIN_TASK_NODE_CLASSIFICATION, BUILTIN_TASK_NODE_REGRESSION, BUILTIN_TASK_EDGE_CLASSIFICATION, @@ -409,13 +410,14 @@ def test_parse_config(): setattr(config, "_task_type", BUILTIN_TASK_NODE_CLASSIFICATION) setattr(config, "_target_ntype", target_ntype) setattr(config, "_multi_tasks", None) - node_id_mapping, predict_dir, emb_dir, pred_ntypes, pred_etypes = _parse_gs_config(config) + node_id_mapping, predict_dir, emb_dir, task_emb_dirs, pred_ntypes, pred_etypes = _parse_gs_config(config) assert node_id_mapping == os.path.join(tmpdirname, "raw_id_mappings") assert predict_dir == save_prediction_path assert emb_dir == save_embed_path assert len(pred_ntypes) == 1 assert pred_ntypes[0] == target_ntype assert len(pred_etypes) == 0 + assert len(task_emb_dirs) == 0 target_etype = ["n0,r0,n1"] config = GSConfig.__new__(GSConfig) @@ -426,13 +428,14 @@ def test_parse_config(): setattr(config, "_target_etype", target_etype) setattr(config, "_multi_tasks", None) - node_id_mapping, predict_dir, emb_dir, pred_ntypes, pred_etypes = _parse_gs_config(config) + node_id_mapping, predict_dir, emb_dir, task_emb_dirs, pred_ntypes, pred_etypes = _parse_gs_config(config) assert node_id_mapping == os.path.join(tmpdirname, "raw_id_mappings") assert predict_dir == save_prediction_path assert emb_dir == save_embed_path assert len(pred_ntypes) == 0 assert len(pred_etypes) == 1 assert pred_etypes[0] == ["n0", "r0", "n1"] + assert len(task_emb_dirs) == 0 # multi-task config multi_task_config = [ @@ -470,9 +473,10 @@ def test_parse_config(): "link_prediction" : { "num_negative_edges": 4, "batch_size": 128, - "exclude_training_targets": False + "exclude_training_targets": False, + "lp_embed_normalizer": "l2_norm" } - } + }, ] config = GSConfig.__new__(GSConfig) @@ -480,7 +484,7 @@ def test_parse_config(): setattr(config, "_save_prediction_path", save_prediction_path) setattr(config, "_save_embed_path", save_embed_path) config._parse_multi_tasks(multi_task_config) - node_id_mapping, predict_dir, emb_dir, pred_ntypes, pred_etypes = _parse_gs_config(config) + node_id_mapping, predict_dir, emb_dir, task_emb_dirs, pred_ntypes, pred_etypes = _parse_gs_config(config) assert node_id_mapping == os.path.join(tmpdirname, "raw_id_mappings") assert isinstance(predict_dir, tuple) @@ -498,14 +502,20 @@ def test_parse_config(): assert len(pred_etypes) == 2 assert pred_etypes[0] == ['n0', 'r0', 'r1'] assert pred_etypes[1] == ['n0', 'r0', 'r2'] + print(task_emb_dirs) + assert len(task_emb_dirs) == 1 + assert task_emb_dirs[0] == get_mttask_id( + task_type="link_prediction", + etype="ALL_ETYPE") # there is no predict path # it will use emb_path + multi_task_config[4]["link_prediction"].pop("lp_embed_normalizer") config = GSConfig.__new__(GSConfig) setattr(config, "_part_config", part_path) setattr(config, "_save_embed_path", save_embed_path) config._parse_multi_tasks(multi_task_config) - node_id_mapping, predict_dir, emb_dir, pred_ntypes, pred_etypes = _parse_gs_config(config) + node_id_mapping, predict_dir, emb_dir, task_emb_dirs, pred_ntypes, pred_etypes = _parse_gs_config(config) assert node_id_mapping == os.path.join(tmpdirname, "raw_id_mappings") assert isinstance(predict_dir, tuple) node_predict_dirs, edge_predict_dirs = predict_dir @@ -515,12 +525,13 @@ def test_parse_config(): assert node_predict_dirs[1] == os.path.join(save_embed_path, config.multi_tasks[1].task_id) assert edge_predict_dirs[0] == os.path.join(save_embed_path, config.multi_tasks[2].task_id) assert edge_predict_dirs[1] == os.path.join(save_embed_path, config.multi_tasks[3].task_id) + assert len(task_emb_dirs) == 0 # there is no predict path and emb path config = GSConfig.__new__(GSConfig) setattr(config, "_part_config", part_path) config._parse_multi_tasks(multi_task_config) - node_id_mapping, predict_dir, emb_dir, pred_ntypes, pred_etypes = _parse_gs_config(config) + node_id_mapping, predict_dir, emb_dir, task_emb_dirs, pred_ntypes, pred_etypes = _parse_gs_config(config) assert predict_dir is None assert emb_dir is None diff --git a/tests/unit-tests/test_gnn.py b/tests/unit-tests/test_gnn.py index eaeda9f48a..4141651ce4 100644 --- a/tests/unit-tests/test_gnn.py +++ b/tests/unit-tests/test_gnn.py @@ -31,6 +31,7 @@ from numpy.testing import assert_almost_equal, assert_equal import dgl +from dgl.distributed import DistTensor from graphstorm.config import GSConfig, TaskInfo from graphstorm.config import BUILTIN_LP_DOT_DECODER @@ -39,7 +40,8 @@ BUILTIN_TASK_EDGE_CLASSIFICATION, BUILTIN_TASK_EDGE_REGRESSION, BUILTIN_TASK_LINK_PREDICTION, - BUILTIN_TASK_RECONSTRUCT_NODE_FEAT) + BUILTIN_TASK_RECONSTRUCT_NODE_FEAT, + GRAPHSTORM_LP_EMB_L2_NORMALIZATION) from graphstorm.model import GSNodeEncoderInputLayer, RelationalGCNEncoder from graphstorm.model import GSgnnNodeModel, GSgnnEdgeModel from graphstorm.model import GSLMNodeEncoderInputLayer, GSPureLMNodeInputLayer @@ -1815,6 +1817,121 @@ class DummyLPPredLoss(nn.Module): def forward(self, pos_score, neg_score): return pos_score["n0"] + neg_score["n0"] +def test_multi_task_norm_node_embs(): + mt_model = GSgnnMultiTaskSharedEncoderModel(0.1) + mt_model.add_task("nc_task", + BUILTIN_TASK_NODE_CLASSIFICATION, + DummyNCDecoder(), + DummyPredLoss(), + "") + mt_model.add_task("nr_task", + BUILTIN_TASK_NODE_REGRESSION, + DummyNRDecoder(), + DummyPredLoss(), + GRAPHSTORM_LP_EMB_L2_NORMALIZATION) + + mt_model.add_task("ec_task", + BUILTIN_TASK_EDGE_CLASSIFICATION, + DummyECDecoder(), + DummyPredLoss(), + "") + + mt_model.add_task("er_task", + BUILTIN_TASK_EDGE_REGRESSION, + DummyERDecoder(), + DummyPredLoss(), + GRAPHSTORM_LP_EMB_L2_NORMALIZATION) + + mt_model.add_task("lp_task", + BUILTIN_TASK_LINK_PREDICTION, + DummyLPDecoder(), + DummyLPPredLoss(), + "") + + mt_model.add_task("lp_task2", + BUILTIN_TASK_LINK_PREDICTION, + DummyLPDecoder(), + DummyLPPredLoss(), + GRAPHSTORM_LP_EMB_L2_NORMALIZATION) + + embs = { + "n0": th.rand((10,16)), + "n1": th.rand((20,16)) + } + norm_embs = { + "n0": F.normalize(embs["n0"]), + "n1": F.normalize(embs["n1"]) + } + + new_embs = mt_model.normalize_task_node_embs("nc_task", embs, inplace=False) + assert_equal(embs["n0"].numpy(), new_embs["n0"].numpy()) + assert_equal(embs["n1"].numpy(), new_embs["n1"].numpy()) + + new_embs = mt_model.normalize_task_node_embs("nr_task", embs, inplace=False) + assert_equal(norm_embs["n0"].numpy(), new_embs["n0"].numpy()) + assert_equal(norm_embs["n1"].numpy(), new_embs["n1"].numpy()) + + new_embs = mt_model.normalize_task_node_embs("ec_task", embs, inplace=False) + assert_equal(embs["n0"].numpy(), new_embs["n0"].numpy()) + assert_equal(embs["n1"].numpy(), new_embs["n1"].numpy()) + + new_embs = mt_model.normalize_task_node_embs("er_task", embs, inplace=False) + assert_equal(norm_embs["n0"].numpy(), new_embs["n0"].numpy()) + assert_equal(norm_embs["n1"].numpy(), new_embs["n1"].numpy()) + + inplace_emb = { + "n0": th.clone(embs["n0"]), + "n1": th.clone(embs["n1"]) + } + mt_model.normalize_task_node_embs("lp_task", inplace_emb, inplace=True) + assert_equal(embs["n0"].numpy(), inplace_emb["n0"].numpy()) + assert_equal(embs["n1"].numpy(), inplace_emb["n1"].numpy()) + + mt_model.normalize_task_node_embs("lp_task2", inplace_emb, inplace=True) + assert_equal(norm_embs["n0"].numpy(), inplace_emb["n0"].numpy()) + assert_equal(norm_embs["n1"].numpy(), inplace_emb["n1"].numpy()) + +def test_multi_task_norm_node_embs_dist(): + mt_model = GSgnnMultiTaskSharedEncoderModel(0.1) + mt_model.add_task("lp_task", + BUILTIN_TASK_LINK_PREDICTION, + DummyLPDecoder(), + DummyLPPredLoss(), + "") + + mt_model.add_task("lp_task2", + BUILTIN_TASK_LINK_PREDICTION, + DummyLPDecoder(), + DummyLPPredLoss(), + GRAPHSTORM_LP_EMB_L2_NORMALIZATION) + + with tempfile.TemporaryDirectory() as tmpdirname: + # get the test dummy distributed graph + g, _ = generate_dummy_dist_graph(tmpdirname, size="tiny") + + embs = {} + norm_embs = {} + dist_embs = {} + + for ntype in g.ntypes: + embs[ntype] = th.rand(g.number_of_nodes(ntype), 16) + norm_embs[ntype] = F.normalize(embs[ntype]) + dist_embs[ntype] = DistTensor((g.number_of_nodes(ntype), 16), + dtype=th.float32, name=f'ntype-{ntype}', + part_policy=g.get_node_partition_policy(ntype)) + dist_embs[ntype][th.arange(g.number_of_nodes(ntype))] = embs[ntype][:] + + new_embs = mt_model.normalize_task_node_embs("lp_task", dist_embs, inplace=False) + for ntype in g.ntypes: + assert_equal(embs[ntype].numpy(), new_embs[ntype][th.arange(g.number_of_nodes(ntype))].numpy()) + + new_embs = mt_model.normalize_task_node_embs("lp_task2", dist_embs, inplace=False) + for ntype in g.ntypes: + assert_equal(norm_embs[ntype].numpy(), new_embs[ntype][th.arange(g.number_of_nodes(ntype))].numpy()) + + dgl.distributed.kvstore.close_kvstore() + + def test_multi_task_forward(): mt_model = GSgnnMultiTaskSharedEncoderModel(0.1) @@ -1850,7 +1967,7 @@ def check_forward(mock_normalize_node_embs, mock_compute_emb, mock_input_embed): - def normalize_size_effect_func(embs): + def normalize_size_effect_func(task_id, embs): return embs def compute_side_effect_func(blocks, node_feats, input_nodes): @@ -1981,7 +2098,7 @@ def check_forward(mock_normalize_node_embs, mock_compute_emb, mock_input_embed): - def normalize_size_effect_func(embs): + def normalize_size_effect_func(task_id, embs): return embs def compute_side_effect_func(blocks, node_feats, input_nodes): @@ -2315,6 +2432,8 @@ def check_call_gen_embs(skip_last_self_loop): if __name__ == '__main__': test_node_feat_reconstruct() + test_multi_task_norm_node_embs() + test_multi_task_norm_node_embs_dist() test_multi_task_forward() test_multi_task_predict() test_multi_task_mini_batch_predict() diff --git a/tests/unit-tests/util.py b/tests/unit-tests/util.py index 963f68f23d..39d3672dee 100644 --- a/tests/unit-tests/util.py +++ b/tests/unit-tests/util.py @@ -108,6 +108,12 @@ def __init__(self, encoder_model, decoders, has_sparse=False): super(DummyGSgnnMTModel, self).__init__(encoder_model, has_sparse) self._decoders = decoders + @property + def node_embed_norm_methods(self): + return {} + + def normalize_task_node_embs(self, task_id, embs, inplace=False): + return embs def forward(self, task_mini_batches): pass diff --git a/training_scripts/gsgnn_mt/ml_nc_lp_norm.yaml b/training_scripts/gsgnn_mt/ml_nc_lp_norm.yaml new file mode 100644 index 0000000000..261a1c6106 --- /dev/null +++ b/training_scripts/gsgnn_mt/ml_nc_lp_norm.yaml @@ -0,0 +1,66 @@ +--- +version: 1.0 +gsf: + basic: + backend: gloo + verbose: false + save_perf_results_path: null + batch_size: 32 + node_feat_name: + - user:feat + - movie:title + gnn: + model_encoder_type: rgcn + fanout: "4" + num_layers: 1 + hidden_size: 32 + use_mini_batch_infer: true + input: + restore_model_path: null + output: + save_model_path: null + save_embed_path: null + hyperparam: + dropout: 0. + lr: 0.001 + lm_tune_lr: 0.0001 + num_epochs: 3 + wd_l2norm: 0 + no_validation: false + rgcn: + num_bases: -1 + use_self_loop: true + sparse_optimizer_lr: 1e-2 + use_node_embeddings: false + multi_task_learning: + - node_classification: + target_ntype: "movie" + label_field: "label" + multilabel: false + num_classes: 19 + batch_size: 16 # will overwrite the global batch_size + mask_fields: + - "train_mask_c0" # node classification mask 0 + - "val_mask_c0" + - "test_mask_c0" + task_weight: 1.0 + eval_metric: + - "accuracy" + - link_prediction: + lp_loss_func: "contrastive" + num_negative_edges: 4 + num_negative_edges_eval: 100 + train_negative_sampler: joint + eval_etype: + - "user,rating,movie" + train_etype: + - "user,rating,movie" + exclude_training_targets: true + reverse_edge_types_map: + - user,rating,rating-rev,movie + batch_size: 128 # will overwrite the global batch_size + mask_fields: + - "train_mask_field_lp" + - "val_mask_field_lp" + - null # empty means there is no test mask + task_weight: 1.0 From 1ca1424b8589ade0b0c4ac8a132319957fa26724 Mon Sep 17 00:00:00 2001 From: jalencato Date: Tue, 30 Jul 2024 11:48:01 -0700 Subject: [PATCH 4/4] [Doc] Graph Construction PR Refactor - PR2: GPartition Doc (#918) *Issue #, if available:* *Description of changes:* Preview version: https://jalencato-graphstorm-doc.readthedocs.io/en/gsprocessing-gpartition-doc/graph-construction/index.html By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Theodore Vasiloudis Co-authored-by: Jian Zhang (James) <6593865@qq.com> Co-authored-by: xiang song(charlie.song) --- .../gs-processing/example.rst | 111 +++++++++++---- .../gspartition/ec2-clusters.rst | 119 ++++++++++++++++ .../gs-processing/gspartition/index.rst | 28 ++++ .../gs-processing/gspartition/sagemaker.rst | 134 ++++++++++++++++++ .../gs-processing/index.rst | 17 ++- .../gs-processing/prerequisites/index.rst | 2 + tutorial/distributed_construction.png | Bin 0 -> 141499 bytes 7 files changed, 383 insertions(+), 28 deletions(-) create mode 100644 docs/source/graph-construction/gs-processing/gspartition/ec2-clusters.rst create mode 100644 docs/source/graph-construction/gs-processing/gspartition/index.rst create mode 100644 docs/source/graph-construction/gs-processing/gspartition/sagemaker.rst create mode 100644 tutorial/distributed_construction.png diff --git a/docs/source/graph-construction/gs-processing/example.rst b/docs/source/graph-construction/gs-processing/example.rst index 23afa10ddd..14d2d0e984 100644 --- a/docs/source/graph-construction/gs-processing/example.rst +++ b/docs/source/graph-construction/gs-processing/example.rst @@ -1,41 +1,54 @@ .. _distributed_construction_example: -GraphStorm Processing Example -============================= +A GraphStorm Distributed Graph Construction Example +=================================================== -To demonstrate how to use the library locally we will +GraphStorm's distributed graph construction is involved with multiple steps. +To help users better understand these steps, we provide an example of distributed graph construction, +which can run locally in one instance. + +To demonstrate how to use distributed graph construction locally we will use the same example data as we use in our unit tests, which you can find in the project's repository, under ``graphstorm/graphstorm-processing/tests/resources/small_heterogeneous_graph``. -Install example dependencies ----------------------------- +Install dependencies +-------------------- -To run the local example you will need to install the GSProcessing +To run the local example you will need to install the GSProcessing and GraphStorm library to your Python environment, and you'll need to clone the -GraphStorm repository to get access to the data. +GraphStorm repository to get access to the data, and DGL tool for GSPartition. Follow the :ref:`gsp-installation-ref` guide to install the GSProcessing library. -You can clone the repository using +To run GSPartition job, you can install the dependencies as following: .. code-block:: bash + pip install graphstorm + pip install pydantic + pip install torch==2.1.0 --index-url https://download.pytorch.org/whl/cpu + pip install dgl==1.1.3 -f https://data.dgl.ai/wheels-internal/repo.html git clone https://github.com/awslabs/graphstorm.git + cd graphstorm + git clone --branch v1.1.3 https://github.com/dmlc/dgl.git You can then navigate to the ``graphstorm-processing/`` directory that contains the relevant data: .. code-block:: bash - cd ./graphstorm/graphstorm-processing/ + cd ./graphstorm-processing/ Expected file inputs and configuration -------------------------------------- +The example will include GSProcessing as the first step and GSPartition as the second step. + GSProcessing expects the input files to be in a specific format that will allow us to perform the processing and prepare the data for partitioning and training. +GSPartition then takes the output of GSProcessing to produce graph data in DistDGLGraph format for training or inference.. The data files are expected to be: @@ -202,8 +215,8 @@ For more details on the re-partitioning step see .. _gsp-examining-output: -Examining the job output ------------------------- +Examining the job output of GSProcessing +------------------------------------------ Once the processing and re-partitioning jobs are done, we can examine the outputs they created. The output will be @@ -281,28 +294,74 @@ in an ``edge_data`` directory. for node id 1 etc. -At this point you can use the DGL distributed partitioning pipeline -to partition your data, as described in the -`DGL documentation `_ -. +Run a GSPartition job locally +------------------------------ +While :ref:`GSPartition` is designed to run on a multi-machine cluster, +you can run GSPartition job locally for the example. Once you have completed the installation +and the GSProcessing example described in the previous section, you can proceed to run the GSPartition step. + +Assuming your working directory is ``graphstorm``, +you can use the following command to run the partition job locally: + +.. code:: bash + + echo 127.0.0.1 > ip_list.txt + python3 -m graphstorm.gpartition.dist_partition_graph \ + --input-path /tmp/gsprocessing-example/ \ + --metadata-filename updated_row_counts_metadata.json \ + --output-path /tmp/gspartition-example/ \ + --num-parts 2 \ + --dgl-tool-path ./dgl/tools \ + --partition-algorithm random \ + --ip-config ip_list.txt -To simplify the process of partitioning and training, without the need -to manage your own infrastructure, we recommend using GraphStorm's -`SageMaker wrappers `_ -that do all the hard work for you and allow -you to focus on model development. In particular you can follow the GraphStorm documentation to run -`distributed partitioning on SageMaker `_. +The command above will first do graph partitioning to determine the ownership for each partition and save the results. +Then it will do data dispatching to physically assign the partitions to graph data and dispatch them to each machine. +Finally it will generate the graph data ready for training/inference. +Examining the job output of GSPartition +--------------------------------------- -To run GSProcessing jobs on Amazon SageMaker we'll need to follow -:ref:`GSProcessing distributed setup` to set up our environment -and :ref:`Running GSProcessing on SageMaker` to execute the job. +Once the partition job is done, you can examine the outputs. + +.. code-block:: bash + $ cd /tmp/gspartition-example + $ ls -ltR + + dist_graph/ + metadata.json + |- part0/ + edge_feat.dgl + graph.dgl + node_feat.dgl + orig_eids.dgl + orig_nids.dgl + partition_assignment/ + director.txt + genre.txt + movie.txt + partition_meta.json + user.txt + +The ``dist_graph`` folder contains partitioned graph ready for training and inference. + +* ``part0``: As we only specify 1 partition in the previous command, we have one part folder here. +There are five files for the partition + * ``edge_feat.dgl``: The edge features for part 0 stored in binary format. + * ``graph.dgl``: The graph structure data for part 0 stored in binary format. + * ``node_feat.dgl``: The node features data for part 0 stored in binary format. + * ``orig_eids.dgl``: The mapping for edges between raw edge IDs and the partitioned graph edge IDs. + * ``orig_nids.dgl``: The mapping for nodes between raw node IDs and the partitioned graph node IDs. + +* ``metadata.json``: This file contains metadata about the distributed DGL graph. + +The ``partition_assignment`` directory contains different partition results for different node types, +which can reused for the `dgl dispatch pipeline `_ .. rubric:: Footnotes .. [#f1] Note that this is just a hint to the Spark engine, and it's not guaranteed that the number of output partitions will always match - the requested value. -.. [#f2] This doc will be future extended to include a partition example. \ No newline at end of file + the requested value. \ No newline at end of file diff --git a/docs/source/graph-construction/gs-processing/gspartition/ec2-clusters.rst b/docs/source/graph-construction/gs-processing/gspartition/ec2-clusters.rst new file mode 100644 index 0000000000..e12253cbcb --- /dev/null +++ b/docs/source/graph-construction/gs-processing/gspartition/ec2-clusters.rst @@ -0,0 +1,119 @@ +====================================== +Running partition jobs on EC2 Clusters +====================================== + +Once the :ref:`distributed processing` is completed, +users can start the partition jobs. This tutorial will provide instructions on how to setup an EC2 cluster and +start GSPartition jobs on it. + +Create a GraphStorm Cluster +---------------------------- + +Setup instances of a cluster +............................. +A cluster contains several instances, each of which runs a GraphStorm Docker container. Before creating a cluster, we recommend to +follow the :ref:`Environment Setup `. The guide shows how to build GraphStorm Docker images, and use a Docker container registry, +e.g. `AWS ECR `_ , to upload the GraphStorm image to an ECR repository, pull it on the instances in the cluster, +and finally start the image as a container. + +.. note:: + + If you are planning to use **parmetis** algorithm, please prepare your docker image using the following instructions: + + .. code-block:: bash + + git clone https://github.com/awslabs/graphstorm.git + + cd /path-to-graphstorm/docker/ + + bash /path-to-graphstorm/docker/build_docker_parmetis.sh /path-to-graphstorm/ image-name image-tag + + There are three positional arguments for ``build_docker_parmetis.sh``: + + 1. **path-to-graphstorm** (**required**), is the absolute path of the "graphstorm" folder, where you cloned the GraphStorm source code. For example, the path could be ``/code/graphstorm``. + 2. **image-name** (optional), is the assigned name of the Docker image to be built . Default is ``graphstorm``. + 3. **image-tag** (optional), is the assigned tag prefix of the Docker image. Default is ``local``. + +Setup a shared file system for the cluster +........................................... +A cluster requires a shared file system, such as NFS or `EFS `_, mounted to each instance in the cluster, in which all GraphStorm containers can share data files, save model artifacts and prediction results. + +`Here `_ is the instruction of setting up an NFS for a cluster. As the steps of setting an NFS could be various on different systems, we suggest users to look for additional information about NFS setting. Here are some available resources: `NFS tutorial `_ by DigitalOcean, `NFS document `_ for Ubuntu. + +For an AWS EC2 cluster, users can also use EFS as the shared file system. Please follow 1) `the instruction of creating EFS `_; 2) `the instruction of installing an EFS client `_; and 3) `the instructions of mounting the EFS filesystem `_ to set up EFS. + +After setting up a shared file system, we can keep all graph data in a shared folder. Then mount the data folder to the ``/path_to_data/`` of each instances in the cluster so that all GraphStorm containers can access the data. + +Run a GraphStorm container +........................... +In each instance, use the following command to start a GraphStorm Docker container and run it as a backend daemon on cpu. + +.. code-block:: shell + + docker run -v /path_to_data/:/data \ + -v /dev/shm:/dev/shm \ + --network=host \ + -d --name test graphstorm:local-cpu service ssh restart + +This command mounts the shared ``/path_to_data/`` folder to a container's ``/data/`` folder by which GraphStorm codes can access graph data and save the partition result. + +Setup the IP Address File and Check Port Status +---------------------------------------------------------- + +Collect the IP address list +........................... +The GraphStorm Docker containers use SSH on port ``2222`` to communicate with each other. Users need to collect all IP addresses of all the instances and put them into a text file, e.g., ``/data/ip_list.txt``, which is like: + +.. figure:: ../../../../../tutorial/distributed_ips.png + :align: center + +.. note:: We recommend to use **private IP addresses** on AWS EC2 cluster to avoid any possible port constraints. + +Put the IP list file into container's ``/data/`` folder. + +Check port +................ +The GraphStorm Docker container uses port ``2222`` to **ssh** to containers running on other machines without password. Please make sure the port is not used by other processes. + +Users also need to make sure the port ``2222`` is open for **ssh** commands. + +Pick one instance and run the following command to connect to the GraphStorm Docker container. + +.. code-block:: bash + + docker container exec -it test /bin/bash + +Users need to exchange the ssh key from each of GraphStorm Docker container to +the rest containers in the cluster: copy the keys from the ``/root/.ssh/id_rsa.pub`` from one container to ``/root/.ssh/authorized_keys`` in containers on all other containers. +In the container environment, users can check the connectivity with the command ``ssh -o StrictHostKeyChecking=no -p 2222``. Please replace the ```` with the real IP address from the ``ip_list.txt`` file above, e.g., + +.. code-block:: bash + + ssh 172.38.12.143 -o StrictHostKeyChecking=no -p 2222 + +If successful, you should login to the container with ip 172.38.12.143. + +If not, please make sure there is no restriction of exposing port 2222. + + +Launch GSPartition Jobs +----------------------- + +Now we can ssh into the **leader node** of the EC2 cluster, and start GSPartition process with the following command: + +.. code:: bash + + python3 -m graphstorm.gpartition.dist_partition_graph + --input-path ${LOCAL_INPUT_DATAPATH} \ + --metadata-filename ${METADATA_FILE} \ + --output-path ${LOCAL_OUTPUT_DATAPATH} \ + --num-parts ${NUM_PARTITIONS} \ + --partition-algorithm ${ALGORITHM} \ + --ip-config ${IP_CONFIG} + +.. warning:: + 1. Please make sure the both ``LOCAL_INPUT_DATAPATH`` and ``LOCAL_OUTPUT_DATAPATH`` are located on the shared filesystem. + 2. The number of instances in the cluster should be equal to ``NUM_PARTITIONS``. + 3. For users who only want to generate partition assignments instead of the partitioned DGL graph, please add ``--partition-assignment-only`` flag. + +Currently we support both ``random`` and ``parmetis`` as the partitioning algorithm for EC2 clusters. diff --git a/docs/source/graph-construction/gs-processing/gspartition/index.rst b/docs/source/graph-construction/gs-processing/gspartition/index.rst new file mode 100644 index 0000000000..1e4032175c --- /dev/null +++ b/docs/source/graph-construction/gs-processing/gspartition/index.rst @@ -0,0 +1,28 @@ +.. _gspartition_index: + +=================================== +Running partition jobs on AWS Infra +=================================== + +GraphStorm Distributed Graph Partition (GSPartition) allows users to do distributed partition on preprocessed graph data +prepared by :ref:`GSProcessing`. To enable distributed training, the preprocessed input data must be converted to a partitioned graph representation. +GSPartition allows user to handle massive graph data in distributed clusters. GSPartition is built on top of the +dgl `distributed graph partitioning pipeline `_. + +GSPartition consists of two steps: Graph Partitioning and Data Dispatching. Graph Partitioning step will assign each node to one partition +and save the results as a set of files called partition assignment. Data Dispatching step will physically partition the +graph data and dispatch them according to the partition assignment. It will generate the graph data in DGL format, ready for distributed training and inference. + +Tutorials for GSPartition are specifically prepared based on AWS infrastructure, +i.e., `Amazon SageMaker `_ and `Amazon EC2 clusters `_. +But, users can create your own clusters easily by following the GSPartition tutorial on Amazon EC2. + +The first section includes instructions on how to run GSPartition on `Amazon SageMaker `_. +The second section includes instructions on how to run GSPartition on `Amazon EC2 clusters `_. + +.. toctree:: + :maxdepth: 1 + :glob: + + sagemaker.rst + ec2-clusters.rst diff --git a/docs/source/graph-construction/gs-processing/gspartition/sagemaker.rst b/docs/source/graph-construction/gs-processing/gspartition/sagemaker.rst new file mode 100644 index 0000000000..946de73ea4 --- /dev/null +++ b/docs/source/graph-construction/gs-processing/gspartition/sagemaker.rst @@ -0,0 +1,134 @@ +========================================== +Running partition jobs on Amazon SageMaker +========================================== + +Once the :ref:`distributed processing` is complete, +you can use Amazon SageMaker launch scripts to launch distributed processing jobs with AWS resources. + +Build the Docker Image for GSPartition Jobs on Amazon SageMaker +--------------------------------------------------------------- +GSPartition job on Amazon SageMaker uses its SageMaker's **BYOC** (Bring Your Own Container) mode. + +Step 1: Build an Amazon SageMaker-compatible Docker image +.......................................................... + +.. note:: + * Please make sure your account has access key (AK) and security access key (SK) configured to authenticate accesses to AWS services, users can refer to `example policy `_. + * For more details of Amazon ECR operation via CLI, users can refer to the `Using Amazon ECR with the AWS CLI document `_. + +First, in a Linux machine, configure a Docker environment by following the `Docker documentation `_ suggestions. + +In order to use the Amazon SageMaker base Docker image, users need to refer the `DLC image command `_ +to find the specific Docker image commands. For example, below is the command for user authentication to access the Amazon SageMaker base Docker image. + +.. code-block:: bash + + aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 763104351884.dkr.ecr.us-east-1.amazonaws.com + +.. note:: + For region other than ``us-east-1``, please refer to `available region `_ + +Secondly, clone GraphStorm source code, and build a GraphStorm SageMaker compatible Docker image from source with commands: + +.. code-block:: bash + + git clone https://github.com/awslabs/graphstorm.git + + cd /path-to-graphstorm/docker/ + + bash build_docker_sagemaker.sh ../ + +The ``build_docker_sagemaker.sh`` script takes four arguments: + +1. **path-to-graphstorm** (**required**), is the path of the ``graphstorm`` folder, where you cloned the GraphStorm source code. For example, the path could be ``/code/graphstorm``. +2. **DEVICE_TYPE**, is the intended device type of the to-be built Docker image. Please specify ``cpu`` for building CPU-compatible images for partition job. +3. **IMAGE_NAME** (optional), is the assigned name of the to-be built Docker image. Default is ``graphstorm``. + +.. warning:: + In order to upload the GraphStorm SageMaker Docker image to Amazon ECR, users need to define the to include the ECR URI string, **.dkr.ecr..amazonaws.com/**, e.g., ``888888888888.dkr.ecr.us-east-1.amazonaws.com/graphstorm``. + +4. **IMAGE_TAG** (optional), is the assigned tag name of the to-be built Docker image. Default is ``sm-``, + that is, ``sm-cpu`` for CPU images. + +Once the ``build_docker_sagemaker.sh`` command completes successfully, there will be a Docker image, named ``:``, +such as ``888888888888.dkr.ecr.us-east-1.amazonaws.com/graphstorm:sm-cpu``, in the local repository, which could be listed by running: + +.. code-block:: bash + + docker images graphstorm + +.. _upload_sagemaker_docker: + +Step 2: Upload Docker images to Amazon ECR repository +....................................................... +Because Amazon SageMaker relies on Amazon ECR to access customers' own Docker images, users need to upload the Docker images built in the Step 1 to their own ECR repository. + +The following command will authenticate the user account to access to user's ECR repository via AWS CLI. + +.. code-block:: bash + + aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com + +Please replace the `` and `` with your own account information and be consistent with the values used in the **Step 1**. + +In addition, users need to create an ECR repository at the specified `` with the name as `` **WITHOUT** the ECR URI string, e.g., ``graphstorm``. + +And then use the following command to push the built GraphStorm Docker image to users' own ECR repository. + +.. code-block:: bash + + docker push : + +Please replace the `` and `` with the actual Docker image name and tag, e.g., ``888888888888.dkr.ecr.us-east-1.amazonaws.com/graphstorm:sm-gpu``. + +Launch the GSPartition Job on Amazon SageMaker +----------------------------------------------- +For this example, we'll use an Amazon SageMaker cluster with 2 ``ml.t3.xlarge`` instances. +We assume the data is already on an AWS S3 bucket. +For large graphs, users can choose larger instances or more instances. + +Install dependencies +..................... +To run GraphStorm with the Amazon SageMaker service, users should install the Amazon SageMaker library and copy GraphStorm's SageMaker tools. + +1. Use the below command to install Amazon SageMaker. + +.. code-block:: bash + + pip install sagemaker + +2. Copy GraphStorm SageMaker tools. Users can clone the GraphStorm repository using the following command or copy the `sagemaker folder `_ to the instance. + +.. code-block:: bash + + git clone https://github.com/awslabs/graphstorm.git + +Launch GSPartition task +........................ +Users can use the following command to launch partition jobs. + +.. code:: bash + + python launch/launch_partition.py \ + --graph-data-s3 ${DATASET_S3_PATH} \ + --num-parts ${NUM_PARTITIONS} \ + --instance-count ${NUM_INSTANCES} \ + --output-data-s3 ${OUTPUT_PATH} \ + --instance-type ${INSTANCE_TYPE} \ + --image-url ${IMAGE_URI} \ + --region ${REGION} \ + --role ${ROLE} \ + --entry-point "run/partition_entry.py" \ + --metadata-filename ${METADATA_FILE} \ + --log-level INFO \ + --partition-algorithm ${ALGORITHM} + +.. warning:: + The ``NUM_INSTANCES`` should be equal to the ``NUM_PARTITIONS`` here. + +Running the above will take the dataset after GSProcessing +from ``${DATASET_S3_PATH}`` as input and create a DistDGL graph with +``${NUM_PARTITIONS}`` under the output path, ``${OUTPUT_PATH}``. +Currently we only support ``random`` as the partitioning algorithm for sagemaker. + + diff --git a/docs/source/graph-construction/gs-processing/index.rst b/docs/source/graph-construction/gs-processing/index.rst index ca239f9bc7..8635b00ffc 100644 --- a/docs/source/graph-construction/gs-processing/index.rst +++ b/docs/source/graph-construction/gs-processing/index.rst @@ -4,13 +4,25 @@ Distributed Graph Construction Beyond single-machine graph construction, distributed graph construction offers enhanced scalability and efficiency. This process involves two main steps: GraphStorm Distributed Data Processing (GSProcessing) -and GraphStorm Distributed Data Partitioning (GPartition). The documentations of GPartition will be released soon. +and GraphStorm Distributed Graph Partitioning (GSPartition). GSProcessing will preprocess the raw data into structured +datasets, and GSPartition will process these structured datasets to create multiple partitions in +`DGL format `_. + +Here is an overview of the workflow for distributed graph construction: + +* **Prepare input data**: GraphStorm Distributed Construction accepts tabular files in parquet/CSV format. +* **Run GSProcessing**: Use GSProcessing to process the input data. This step prepares the data for partitioning including edge and node data, transformation details, and node id mappings. +* **Run GSPartition**: Use GSPartition to partition the processed data into graph files suitable for distributed training. + +.. figure:: ../../../../tutorial/distributed_construction.png + :align: center The following sections provide guidance on doing distributed graph construction. The first section details the execution environment setup for GSProcessing. The second section offers examples on drafting a configuration file for a GSProcessing job. The third section explains how to deploy your GSProcessing job with AWS infrastructure. -The final section shows an example to quick start GSProcessing. +The fourth section includes how to do GSPartition with the GSProcessing output. +The final section shows an example to quickly start GSProcessing and GSPartition. .. toctree:: :maxdepth: 1 @@ -19,4 +31,5 @@ The final section shows an example to quick start GSProcessing. prerequisites/index.rst input-configuration.rst aws-infra/index.rst + gspartition/index.rst example.rst \ No newline at end of file diff --git a/docs/source/graph-construction/gs-processing/prerequisites/index.rst b/docs/source/graph-construction/gs-processing/prerequisites/index.rst index 1e1cf534f7..7951c26994 100644 --- a/docs/source/graph-construction/gs-processing/prerequisites/index.rst +++ b/docs/source/graph-construction/gs-processing/prerequisites/index.rst @@ -1,3 +1,5 @@ +.. _gsprocessing_prerequisites_index: + =============================================== Distributed GraphStorm Processing =============================================== diff --git a/tutorial/distributed_construction.png b/tutorial/distributed_construction.png new file mode 100644 index 0000000000000000000000000000000000000000..357bae2f9418643b3a02e4e37ba2134f700ebeee GIT binary patch literal 141499 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 z%0A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 @3OfTSV^W9#r>-*pTzSRo$HhSRoPk;K81sFU~ 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�TCRB{7#K@1_zmQvTabHqcPsEr_H9 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 literal 0 HcmV?d00001