Skip to main content

Deep Graph Library (DGL)

· 5 min read
Ahmed BARGADY
PhD Student

This page summarizes the core concepts of the Deep Graph Library (DGL), focusing on graph construction, feature handling, and the message-passing paradigm used to implement architectures like GCN and GAT.

Source

Based on the official Deep Graph Library Documentation

1. Creating a Graph (DGLGraph)

In DGL, a graph is defined by a set of edges connecting nodes. Nodes are identified by integers starting from 0.

Basic Construction

You define edges using two arrays: Source Nodes (u) and Destination Nodes (v).

import dgl
import torch

# Define a graph with 4 nodes (0, 1, 2, 3)
# Edges: 0->1, 1->2, 2->3, 3->0 (A cycle)
u = torch.tensor([0, 1, 2, 3]) # Source nodes
v = torch.tensor([1, 2, 3, 0]) # Destination nodes

# Create the graph
g = dgl.graph((u, v))

print(f"Number of nodes: {g.num_nodes()}")
print(f"Number of edges: {g.num_edges()}")
Bi-directional Graphs

DGL graphs are directed by default. To make them undirected (bi-directional), you must explicitly add reverse edges or use dgl.add_reverse_edges(g).

2. Node and Edge Features

Graphs in DGL are not just structure; they store data. You can store feature vectors (embeddings) directly on the nodes and edges using the ndata and edata interfaces.

# Assign a 5-dimensional random vector to each node
# 'feat' is just a name we choose. You can name it 'h', 'attr', 'x', etc.
g.ndata['feat'] = torch.randn(4, 5)

# Assign a 2-dimensional vector to each edge (e.g., edge type embeddings)
g.edata['weight'] = torch.randn(4, 2)

# Accessing features of Node 0
print(g.ndata['feat'][0])

3. Message Passing (The Core Engine)

This is the most critical concept in DGL. It abstracts the mathematical formula of GNNs.

Mathematical Formulation

hv(l+1)=σ(uN(v)Message(hu(l),hv(l),euv))h_v^{(l+1)} = \sigma \left( \sum_{u \in \mathcal{N}(v)} \text{Message}(h_u^{(l)}, h_v^{(l)}, e_{uv}) \right)

DGL uses a specific syntax for this: update_all(message_func, reduce_func).

Built-in Functions (dgl.function)

DGL provides optimized C++ kernels for common operations, accessed via dgl.function (usually imported as fn).

Function NameDescriptionNotation
fn.copy_uCopy source node features to be the message.umu \to m
fn.u_add_eAdd source feature and edge feature.u+emu + e \to m
fn.u_mul_eMultiply source feature by edge feature (Attention).u×emu \times e \to m
fn.sumSum up all incoming messages.mh\sum m \to h
fn.meanAverage all incoming messages.1Nmh\frac{1}{N} \sum m \to h

Implementations

A Simple Graph Convolution

Let's implement a basic layer where every node calculates the sum of its neighbors' features.

import dgl.function as fn

# 1. Define the Message: "copy_u" means "Copy the neighbor's 'feat' vector"
# 2. Define the Reduce: "sum" means "Sum up those vectors"
g.update_all(fn.copy_u('feat', 'm'), fn.sum('m', 'h_new'))

# The result is stored in 'h_new' in ndata
print(g.ndata['h_new'])

Edge-Weighted Message Passing (GAT Style)

In architectures like Graph Attention Networks, we care about the edge data.

# 1. Message: Multiply Source Feature ('feat') by Edge Weight ('weight')
# This is analogous to the Attention Mechanism
g.update_all(fn.u_mul_e('feat', 'weight', 'm'), fn.sum('m', 'h_weighted'))

4. Building a GNN Module

To use DGL in a real project, you wrap the message passing logic inside a standard PyTorch nn.Module.

Here is a simple Graph Convolutional Layer (GCN) implementation:

import torch.nn as nn

class GCNLayer(nn.Module):
def __init__(self, in_feats, out_feats):
super(GCNLayer, self).__init__()
# A linear layer to transform features before aggregation
self.linear = nn.Linear(in_feats, out_feats)

def forward(self, g, feature):
# 1. Create a local scope to avoid dirtying the graph
with g.local_scope():
# 2. Add the current features to the graph
g.ndata['h'] = feature

# 3. Message Passing:
# Send copy of 'h', Sum them up, store as 'h_agg'
g.update_all(fn.copy_u('h', 'm'), fn.sum('m', 'h_agg'))

# 4. Retrieve the aggregated result
h_agg = g.ndata['h_agg']

# 5. Pass through Linear Layer and return
return self.linear(h_agg)

5. Training Loop

Training a DGL model looks almost identical to training a standard image or text model in PyTorch.

Key Difference

In the forward pass, you must pass both the graph object (g) and the features (features).

# Initialize graph and features
g = ...
features = g.ndata['feat']
labels = ...

# Initialize Model, Loss, and Optimizer
model = GCNLayer(in_feats=5, out_feats=2)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

# Training Loop
for epoch in range(100):
model.train()

# Forward Pass
logits = model(g, features)

# Calculate Loss (e.g., CrossEntropy)
loss = F.cross_entropy(logits, labels)

# Backward Pass
optimizer.zero_grad()
loss.backward()
optimizer.step()

print(f"Epoch {epoch} | Loss: {loss.item()}")

6. Advanced: Batching Graphs

When processing multiple small graphs (e.g., in a dataset of chemical molecules or system logs), DGL handles this by merging many small graphs into one giant Batched Graph.

# List of individual graphs
graphs = [g1, g2, g3, ...]

# Batch them into one object
batched_g = dgl.batch(graphs)

# You can run the EXACT same model code on this batched graph!
output = model(batched_g, batched_g.ndata['feat'])

# To get separate outputs again (Readout/Pooling)
# This sums up all nodes belonging to Graph 1, all nodes for Graph 2, etc.
graph_level_embeddings = dgl.sum_nodes(batched_g, output)

Notebook

You can find a simple example of using DGL for GCN here on Colab.

Resources