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 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
|
\newpage
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Performance} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\label{perf}
Getting the best performance out of an algorithm that relies on GraphBLAS can
depend on many factors. This section describes some of the possible
performance pitfalls you can hit when using SuiteSparse:GraphBLAS, and how to
avoid them (or at least know when you've encountered them).
%-------------------------------------------------------------------------------
\subsection{The burble is your friend}
%-------------------------------------------------------------------------------
Turn on the burble with \verb'GrB_set (GrB_GLOBAL, true, GxB_BURBLE)'. You will get a
single line of output from each (significant) call to GraphBLAS.
The burble output can help you detect when you are likely using sub-optimal
methods, as described in the next sections.
When the JIT is in use the burble reports when a JIT kernel is run (which
is quick), loaded for the first time (which takes a small amount of time),
and when a JIT kernels is compiled (which can take a few tenths of a second
or more). The compiler command is printed in full. If you encounter a
compiler error, you can cut-and-paste the compiler command while outside
of your application to help track down the compiler error.
%-------------------------------------------------------------------------------
\subsection{Data types and typecasting: use the JIT}
%-------------------------------------------------------------------------------
If the JIT is disabled,
avoid mixing data types and relying on typecasting as much as possible.
SuiteSparse:GraphBLAS has a set of highly-tuned kernels for each data type,
and many operators and semirings, but there are too many combinations to
generate ahead of time. If typecasting is required, or if
SuiteSparse:GraphBLAS does not have a kernel for the specific operator or
semiring, the word \verb'generic' will appear in the burble. The generic
methods rely on function pointers for each operation on every scalar, so they
are slow. Enabling the JIT avoids this problem, since GraphBLAS can then
compile kernel specific to the types used.
Without the JIT,
the only time that typecasting is fast is when computing \verb'C=A' via
\verb'GrB_assign' or \verb'GrB_apply', where the data types of \verb'C' and
\verb'A' can differ. In this case, one of $13^2 = 169$ kernels are called,
each of which performs the specific typecasting requested, without relying on
function pointers.
%-------------------------------------------------------------------------------
\subsection{Matrix data structures: sparse, hypersparse, bitmap, or full}
%-------------------------------------------------------------------------------
SuiteSparse:GraphBLAS tries to automatically determine the best data structure
for your matrices and vectors, selecting between sparse, hypersparse, bitmap,
and full formats. By default, all 4 formats can be used. A matrix typically
starts out hypersparse when it is created by \verb'GrB_Matrix_new', and then
changes during its lifetime, possibly taking on all four different formats
at different times. This can be modified via \verb'GrB_set'. For example,
this line of code:
{\footnotesize
\begin{verbatim}
GrB_set (A, GxB_SPARSE + GxB_BITMAP, GxB_SPARSITY_CONTROL) ; \end{verbatim}}
\noindent
tells SuiteSparse that the matrix \verb'A' can be held in either sparse or
bitmap format (at its discretion), but not hypersparse or full. The bitmap
format will be used if the matrix has enough entries, or sparse otherwise.
Sometimes this selection is best controlled by the user algorithm, so a single
format can be requested:
{\footnotesize
\begin{verbatim}
GrB_set (A, GxB_SPARSE, GxB_SPARSITY_CONTROL) ; \end{verbatim}}
This ensures that SuiteSparse will primarily use the sparse format. This is
still just a hint, however. The data structure is opaque and SuiteSparse is
free to choose otherwise. In particular, if you insist on using only the
\verb'GxB_FULL' format, then that format is used when all entries are present.
However, if the matrix is not actually full with all entries present, then the
bitmap format is used instead. The full format does not preserve the sparsity
structure in this case. Any GraphBLAS library must preserve the proper
structure, per the C Specification. This is critical in a graph algorithm,
since an edge $(i,j)$ of weight zero, say, is not the same as no edge $(i,j)$
at all.
%-------------------------------------------------------------------------------
\subsection{Matrix formats: by row or by column, or using the transpose of
a matrix}
%-------------------------------------------------------------------------------
By default, SuiteSparse uses a simple rule:
all matrices are held by row, unless the consist of a single
column, in which case they are held by column. All vectors are treated as if
they are $n$-by-1 matrices with a single column. Changing formats from
row-oriented to column-oriented can have significant performance implications,
so SuiteSparse never tries to outguess the application. It just uses this
simple rule.
However, there are cases where changing the format can greatly improve
performance. There are two ways to handle this, which in the end are
equivalent in the SuiteSparse internals. You can change the format (row to
column oriented, or visa versa), or work with the explicit transpose of a
matrix in the same storage orientation.
There are cases where SuiteSparse must explicitly transpose an input matrix, or
the output matrix, in order to perform a computation. For example, if all
matrices are held in row-oriented fashion, SuiteSparse does not have a method
for computing \verb"C=A'*B", where \verb'A' is transposed. Thus, SuiteSparse
either computes a temporary transpose of its input matrix \verb'AT=A' and then
\verb'C=AT*B', or it swaps the computations, performing \verb"C=(B'*A)'", which
requires an explicit transpose of \verb'BT=B', and a transpose of the final
result to obtain \verb'C'.
These temporary transposes are costly to compute, taking time and memory. They
are not kept, but are discarded when the method returns to the user
application. If you see the term \verb'transpose' in the burble output, and if
you need to perform this computation many times, try constructing your own
explicit transpose, say \verb"AT=A'", via \verb'GrB_transpose', or create a
copy of \verb'A' but held in another orientation via \verb'GrB_set'. For
example, assuming the default matrix format is by-row, and that \verb'A' is
\verb'm'-by-\verb'n' of type \verb'GrB_FP32':
{\footnotesize
\begin{verbatim}
// method 1: AT = A'
GrB_Matrix_new (AT, GrB_FP32, n, m) ;
GrB_transpose (AT, NULL, NULL, A, NULL) ;
// method 2: A2 = A but held by column instead of by row
// note: doing the set before the assign is faster than the reverse
GrB_Matrix_new (A2, GrB_FP32, m, n) ;
GrB_set (A2, GrB_COLMAJOR, GrB_STORAGE_ORIENTATION_HINT) ;
GrB_assign (A2, NULL, NULL, A, GrB_ALL, m, GrB_ALL, n, NULL) ; \end{verbatim}}
Internally, the data structure for \verb'AT' and \verb'A2' are nearly identical
(that is, the tranpose of \verb'A' held in row format is the same as \verb'A'
held in column format). Using either of them in subsequent calls to GraphBLAS
will allow SuiteSparse to avoid computing an explicit transpose. The two
matrices \verb'AT' and \verb'A2' do differ in one very significant way: their
dimensions are different, and they behave differement mathematically.
Computing \verb"C=A'*B" using these matrices would differ:
{\footnotesize
\begin{verbatim}
// method 1: C=A'*B using AT
GrB_mxm (C, NULL, NULL, semiring, AT, B, NULL) ;
// method 2: C=A'*B using A2
GrB_mxm (C, NULL, NULL, semiring, A2, B, GrB_DESC_T0) ; \end{verbatim}}
The first method computes \verb'C=AT*B'. The second method computes
\verb"C=A2'*B", but the result of both computations is the same, and internally
the same kernels will be used.
%-------------------------------------------------------------------------------
\subsection{Push/pull optimization}
%-------------------------------------------------------------------------------
Closely related to the discussion above on when to use a matrix or its
transpose is the exploitation of ``push/pull'' direction optimization. In
linear algebraic terms, this is simply deciding whether to multiply by the
matrix or its transpose. Examples can be see in the BFS and
Betweeness-Centrality methods of LAGraph. Here is the BFS kernel:
{\footnotesize
\begin{verbatim}
int sparsity = do_push ? GxB_SPARSE : GxB_BITMAP ;
GrB_set (q, sparsity, GxB_SPARSITY_CONTROL) ;
if (do_push)
{
// q'{!pi} = q'*A
GrB_vxm (q, pi, NULL, semiring, q, A, GrB_DESC_RSC) ;
}
else
{
// q{!pi} = AT*q
GrB_mxv (q, pi, NULL, semiring, AT, q, GrB_DESC_RSC) ;
}\end{verbatim}}
The call to \verb'GrB_set' is optional, since SuiteSparse will likely already
determine that a bitmap format will work best when the frontier \verb'q' has
many entries, which is also when the pull step is fastest. The push step
relies on a sparse vector times sparse matrix method originally due to
Gustavson. The output is computed as a set union of all rows \verb'A(i,:)'
where \verb'q(i)' is present on input. This set union is very fast when
\verb'q' is very sparse. The pull step relies on a sequence of dot product
computations, one per possible entry in the output \verb'q', and it uses the
matrix \verb"AT" which is a row-oriented copy of the explicit transpose of the
adjacency matrix \verb'A'.
Mathematically, the results of the two methods are identical, but internally,
the data format of the input matrices is very different (using \verb'A' held
by row, or \verb'AT' held by row which is the same as a copy of \verb'A' that
is held by column), and the algorithms used are very different.
%-------------------------------------------------------------------------------
\subsection{Computing with full matrices and vectors}
%-------------------------------------------------------------------------------
Sometimes the best approach to getting the highest performance is to use dense
vectors, and occassionaly dense matrices are tall-and-thin or short-and-fat.
Packages such as Julia, Octave, or MATLAB, when dealing with the conventional
plus-times semirings, assume that multiplying a sparse matrix \verb'A' times a
dense vector \verb'x', \verb'y=A*x', will result in a dense vector \verb'y'.
This is not always the case, however. GraphBLAS must always return a result
that respects the sparsity structure of the output matrix or vector. If the
$i$th row of \verb'A' has no entries then \verb'y(i)' must not appear as an
entry in the vector \verb'y', so it cannot be held as a full vector. As a
result, the following computation can be slower than it could be:
{\footnotesize
\begin{verbatim}
GrB_mxv (y, NULL, NULL, semiring, A, x, NULL) ; \end{verbatim}}
SuiteSparse must do extra work to compute the sparsity of this vector \verb'y',
but if this is not needed, and \verb'y' can be padded with zeros (or
the identity value of the monoid, to be precise), a faster method can be used,
by relying on the accumulator. Instead of computing \verb'y=A*x', set all
entries of \verb'y' to zero first, and then compute \verb'y+=A*x' where the
accumulator operator and type matches the monoid of the semiring. SuiteSparse
has special kernels for this case; you can see them in the burble as
\verb'F+=S*F' for example.
{\footnotesize
\begin{verbatim}
// y = 0
GrB_assign (y, NULL, NULL, 0, GrB_ALL, n, NULL) ;
// y += A*x
GrB_mxv (y, NULL, GrB_PLUS_FP32, GrB_PLUS_TIMES_SEMIRING_FP32, A, x, NULL) ; \end{verbatim}}
You can see this computation in the LAGraph PageRank method, where all
entries of \verb'r' are set to the \verb'teleport' scalar first.
{\footnotesize
\begin{verbatim}
for (iters = 0 ; iters < itermax && rdiff > tol ; iters++)
{
// swap t and r ; now t is the old score
GrB_Vector temp = t ; t = r ; r = temp ;
// w = t ./ d
GrB_eWiseMult (w, NULL, NULL, GrB_DIV_FP32, t, d, NULL) ;
// r = teleport
GrB_assign (r, NULL, NULL, teleport, GrB_ALL, n, NULL) ;
// r += A'*w
GrB_mxv (r, NULL, GrB_PLUS_FP32, LAGraph_plus_second_fp32, AT, w, NULL) ;
// t -= r
GrB_assign (t, NULL, GrB_MINUS_FP32, r, GrB_ALL, n, NULL) ;
// t = abs (t)
GrB_apply (t, NULL, NULL, GrB_ABS_FP32, t, NULL) ;
// rdiff = sum (t)
GrB_reduce (&rdiff, NULL, GrB_PLUS_MONOID_FP32, t, NULL) ;
} \end{verbatim}}
SuiteSparse exploits the iso-valued property of the scalar-to-vector assignment
of \verb'y=0', or \verb'r=teleport', and performs these assignments in O(1)
time and space. Because the \verb'r' vector start out as full on input to
\verb'GrB_mxv', and because there is an accumulatr with no mask, no entries in
the input/output vector \verb'r' will be deleted, even if \verb'A' has empty
rows. The call to \verb'GrB_mxv' exploits this, and is able to use a fast
kernel for this computation. SuiteSparse does not need to compute the sparsity
pattern of the vector \verb'r'.
%-------------------------------------------------------------------------------
\subsection{Iso-valued matrices and vectors}
%-------------------------------------------------------------------------------
Using iso-valued matrices and vectors is always faster than using matrices and
vectors whose entries can have different values. Iso-valued matrices are very
important in graph algorithms. For example, an unweighted graph is best
represented as an iso-valued sparse matrix, and unweighted graphs are very
common. The burble output, \verb'GxB_print', or \verb'GrB_get'
can all be used to report whether or not your matrix or
vector is iso-valued.
Sometimes a matrix or vector may have values that are all the same, but
SuiteSparse hasn't detected this. If this occurs, you can force a matrix
or vector to be iso-valued by assigning a single scalar to all its entries.
{\footnotesize
\begin{verbatim}
// C<s(C)> = 3.14159
GrB_assign (C, C, NULL, 3.14159, GrB_ALL, m, GrB_ALL, n, GrB_DESC_S) ; \end{verbatim}}
The matrix \verb'C' is used as its own mask. The descriptor is essential here,
telling the mask to be used in a structural sense, without regard to the values
of the entries in the mask. This assignment sets all entries that already
exist in \verb'C' to be equal to a single value, 3.14159. The sparsity
structure of \verb'C' does not change. Of course, any scalar can be used; the
value 1 is common for unweighted graphs. SuiteSparse:GraphBLAS performs the
above assignment in O(1) time and space, independent of the dimension of
\verb'C' or the number of entries in contains.
%-------------------------------------------------------------------------------
\subsection{User-defined types and operators: use the JIT}
%-------------------------------------------------------------------------------
If the JIT is disabled, these will be slow. With the JIT enabled, data types
and operators are just as fast as built-in types and operators. A CUDA JIT for
the GPU is in progress, collaboration with Joe Eaton and Corey Nolet.
A SYCL/OpenCL JIT is under consideration, but work has not yet been started.
%-------------------------------------------------------------------------------
\subsection{About NUMA systems}
%-------------------------------------------------------------------------------
I have tested this package extensively on multicore single-socket systems, but
have not yet optimized it for multi-socket systems with a NUMA architecture.
That will be done in a future release. If you publish benchmarks
with this package, please state the SuiteSparse:GraphBLAS version, and a caveat
if appropriate. If you see significant performance issues when going from a
single-socket to multi-socket system, I would like to hear from you so I can
look into it.
|