File: demo_comm-pattern.py

package info (click to toggle)
fenics-dolfinx 1%3A0.10.0.post4-1exp1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 6,028 kB
  • sloc: cpp: 36,535; python: 25,391; makefile: 226; sh: 171; xml: 55
file content (216 lines) | stat: -rw-r--r-- 6,811 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.13.6
# ---

# # Parallel communication pattern analysis
#
# ```{admonition} Download sources
# :class: download
# * {download}`Python script <./demo_comm-pattern.py>`
# * {download}`Jupyter notebook <./demo_comm-pattern.ipynb>`
# ```
# This demo illustrates how to:
# - Build a graph that represents a parallel communication pattern
# - Analyse the parallel communication pattern using
#   [NetworkX](https://networkx.org/).
#
# The layout of a distributed array across processes (MPI ranks) is
# described in DOLFINx by an {py:class}`IndexMap
# <dolfinx.common.IndexMap>`. An {py:class}`IndexMap
# <dolfinx.common.IndexMap>` represents the range of locally 'owned'
# array indices and the indices that are ghosted on a rank. It also
# holds information on the ranks that the calling rank will send data to
# and ranks that will send data to the caller.
#

# +
import itertools as it
import json

from mpi4py import MPI

import matplotlib.pyplot as plt
import networkx as nx
from matplotlib.ticker import MaxNLocator

from dolfinx import fem, graph, mesh

# -


# The following function plots a directed graph, with the edge weights
# labeled. Each node is an MPI rank, and an edge represents a
# communication edge. The edge weights indicate the volume of data
# communicated.


# +
def plot_graph(G: nx.MultiGraph, egde_labels=False):
    """Plot the communication graph."""
    pos = nx.circular_layout(G)
    nx.draw_networkx_nodes(G, pos, alpha=0.75)
    nx.draw_networkx_labels(G, pos, font_size=12)

    width = 0.5
    edge_color = ["g" if d["local"] == 1 else "grey" for _, _, d in G.edges(data=True)]
    if egde_labels:
        # Curve edges to distinguish between in- and out-edges
        connectstyle = [f"arc3,rad={r}" for r in it.accumulate([0.15] * 4)]

        # Color edges by local (shared memory) or remote (remote memory)
        # communication
        nx.draw_networkx_edges(
            G, pos, width=width, edge_color=edge_color, connectionstyle=connectstyle
        )

        labels = {tuple(edge): f"{attrs['weight']}" for *edge, attrs in G.edges(data=True)}
        nx.draw_networkx_edge_labels(
            G,
            pos,
            labels,
            connectionstyle=connectstyle,
            label_pos=0.5,
            font_color="k",
            bbox={"alpha": 0},
        )
    else:
        nx.draw_networkx_edges(G, pos, width=width, edge_color=edge_color)


# -

# The following function produces bar charts with the number of out-edges
# per rank and the sum of the out edge weights (measure of data
# volume) per rank.


# +
def plot_bar(G: nx.MultiGraph):
    """Plot bars charts with the degree (number of 'out-edges') and the
    outward data volume for each rank.
    """

    ranks = range(G.order())
    num_edges = [len(nbrs) for _, nbrs in G.adj.items()]
    weights = [sum(data["weight"] for nbr, data in nbrs.items()) for _, nbrs in G.adj.items()]

    _fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

    ax1.bar(ranks, num_edges)
    ax1.set_xlabel("rank")
    ax1.set_ylabel("out degree")
    ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
    ax1.yaxis.set_major_locator(MaxNLocator(integer=True))

    ax2.bar(ranks, weights)
    ax2.set_xlabel("rank")
    ax2.set_ylabel("sum of edge weights")
    ax2.xaxis.set_major_locator(MaxNLocator(integer=True))
    ax2.yaxis.set_major_locator(MaxNLocator(integer=True))


# -

# Create a mesh and function space. The function space will build an
# {py:class}`IndexMap <dolfinx.common.IndexMap>` for the
# degree-of-freedom map. The {py:class}`IndexMap
# <dolfinx.common.IndexMap>` describes how the degrees-of-freedom are
# distributed in parallel (across MPI ranks). From information on the
# parallel distribution we will be able to compute the communication
# graph.

# +
msh = mesh.create_box(
    comm=MPI.COMM_WORLD,
    points=[(0.0, 0.0, 0.0), (2.0, 1.0, 1.0)],
    n=(22, 36, 19),
    cell_type=mesh.CellType.tetrahedron,
)
V = fem.functionspace(msh, ("Lagrange", 2))
# -

# The function {py:func}`comm_graph <dolfinx.graph.comm_graph>` builds a
# communication graph that represents data begin sent from the owning
# rank to ranks that ghost the data. We use the degree-of-freedom map's
# `IndexMap`. Building the communication data is collective across MPI
# ranks. However, a non-empty graph is returned only on rank 0.

# +
comm_graph = graph.comm_graph(V.dofmap.index_map)
# -

# A function for printing some communication graph metrics:


# +
def print_stats(G):
    print("Communication graph data:")
    print(f"  Num edges: {G.size()}")
    print(f"  Num local: {G.size('local')}")
    print(f"  Edges weight sum: {G.size('weight')}")
    if G.order() > 0:
        print(f"  Average edges per node: {G.size() / G.order()}")
    if G.size() > 0:
        print(f"  Average edge weight: {G.size('weight') / G.size()}")


# -

# The graph data will be processed on rank 0. From the communication
# graph data, edge and node data for creating a `NetworkX`` graph is build
# using {py:fuc}`comm_graph_data <dolfinx.graph.comm_graph_data>`.
#
# Data for use with `NetworkX` can also be reconstructed from a JSON
# string. The JSON string can be created using {py:func}`comm_to_json
# <dolfinx.graph.comm_to_json>`. This is helpful for cases there a
# simulaton is executed and the graph data is written to file for later
# analysis.

# +
if msh.comm.rank == 0:
    # To create a NetworkX directed graph we build graph data in a form
    # from which we can create a NetworkX graph. Each edge will have a
    # weight and a 'local(1)/remote(0)' memory indicator and each node
    # has its local size and the number of ghosts.
    adj_data, node_data = graph.comm_graph_data(comm_graph)

    print("Test:", graph.comm_graph_data(comm_graph))

    # Create a NetworkX directed graph.
    H = nx.DiGraph()
    H.add_edges_from(adj_data)
    H.add_nodes_from(node_data)

    # Create graph with sorted nodes. This can be helpful for
    # visualisations.
    G = nx.DiGraph()
    G.add_nodes_from(sorted(H.nodes(data=True)))
    G.add_edges_from(H.edges(data=True))

    print_stats(G)

    plot_bar(G)
    plt.show()

    plot_graph(G, True)
    plt.show()

    # Get graph data as a JSON string (useful if running from C++, in
    # which case the JSON string can be written to file)
    data_json_str = graph.comm_to_json(comm_graph)
    H1 = nx.adjacency_graph(json.loads(data_json_str))

    # Create graph with sorted nodes. This can be helpful for
    # visualisations.
    G1 = nx.DiGraph()
    G1.add_nodes_from(sorted(H1.nodes(data=True)))
    G1.add_edges_from(H1.edges(data=True))
    print_stats(G1)

# -