-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathExamplesSpMV.html
More file actions
276 lines (274 loc) · 23.2 KB
/
Copy pathExamplesSpMV.html
File metadata and controls
276 lines (274 loc) · 23.2 KB
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
<!-- HTML header for doxygen 1.13.1-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=11"/>
<meta name="generator" content="Doxygen 1.13.1"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Taskflow: A General-purpose Task-parallel Programming System: Sparse Matrix-Vector Multiplication</title>
<link href="tabs.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="dynsections.js"></script>
<script type="text/javascript" src="clipboard.js"></script>
<link href="navtree.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="navtreedata.js"></script>
<script type="text/javascript" src="navtree.js"></script>
<script type="text/javascript" src="resize.js"></script>
<script type="text/javascript" src="cookie.js"></script>
<link href="search/search.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="search/searchdata.js"></script>
<script type="text/javascript" src="search/search.js"></script>
<link href="doxygen.css" rel="stylesheet" type="text/css" />
<link href="custom.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
<div id="titlearea">
<table cellspacing="0" cellpadding="0">
<tbody>
<tr id="projectrow">
<td id="projectlogo"><img alt="Logo" src="taskflow_logo.png"/></td>
<td id="projectalign">
<div id="projectname"><a href="https://github.com/taskflow/taskflow" style="color:inherit; text-decoration:none;">Taskflow: A General-purpose Task-parallel Programming System</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- end header part -->
<!-- Generated by Doxygen 1.13.1 -->
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT */
var searchBox = new SearchBox("searchBox", "search/",'.html');
/* @license-end */
</script>
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT */
$(function() { codefold.init(0); });
/* @license-end */
</script>
<script type="text/javascript" src="menudata.js"></script>
<script type="text/javascript" src="menu.js"></script>
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT */
$(function() {
initMenu('',true,false,'search.php','Search',true);
$(function() { init_search(); });
});
/* @license-end */
</script>
<div id="main-nav"></div>
</div><!-- top -->
<div id="side-nav" class="ui-resizable side-nav-resizable">
<div id="nav-tree">
<div id="nav-tree-contents">
<div id="nav-sync" class="sync"></div>
</div>
</div>
<div id="splitbar" style="-moz-user-select:none;"
class="ui-resizable-handle">
</div>
</div>
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT */
$(function(){initNavTree('ExamplesSpMV.html',''); initResizable(true); });
/* @license-end */
</script>
<div id="doc-content">
<!-- window showing the filter options -->
<div id="MSearchSelectWindow"
onmouseover="return searchBox.OnSearchSelectShow()"
onmouseout="return searchBox.OnSearchSelectHide()"
onkeydown="return searchBox.OnSearchSelectKey(event)">
</div>
<!-- iframe showing the search results (closed by default) -->
<div id="MSearchResultsWindow">
<div id="MSearchResults">
<div class="SRPage">
<div id="SRIndex">
<div id="SRResults"></div>
<div class="SRStatus" id="Loading">Loading...</div>
<div class="SRStatus" id="Searching">Searching...</div>
<div class="SRStatus" id="NoMatches">No Matches</div>
</div>
</div>
</div>
</div>
<div><div class="header">
<div class="headertitle"><div class="title">Sparse Matrix-Vector Multiplication</div></div>
</div><!--header-->
<div class="contents">
<div class="toc"><h3>Table of Contents</h3>
<ul>
<li class="level1">
<a href="#SpMVIntroduction">What is Sparse Matrix-Vector Multiplication?</a>
</li>
<li class="level1">
<a href="#SpMVCSR">The CSR Storage Format</a>
</li>
<li class="level1">
<a href="#SpMVWhyPartitionerMatters">Why Partitioner Choice Matters</a>
</li>
<li class="level1">
<a href="#SpMVImplementation">Implementation</a>
</li>
<li class="level1">
<a href="#SpMVDesignPoints">Design Points</a>
</li>
</ul>
</div>
<div class="textblock"><p>We implement a parallel sparse matrix-vector multiplication (SpMV) using <a class="el" href="classtf_1_1FlowBuilder.html#a2582a216d54dacca2b7022ea7e89452a" title="constructs a parallel-for task over a one- or multi-dimensional index range">tf::Taskflow::for_each_by_index</a>. This example highlights why irregular workloads like SpMV require careful partitioner selection and demonstrates how different partitioners affect load balance across workers.</p>
<h1><a class="anchor" id="SpMVIntroduction"></a>
What is Sparse Matrix-Vector Multiplication?</h1>
<p>Many scientific and engineering problems involve matrices where the vast majority of entries are zero. Examples include finite element stiffness matrices, graph adjacency matrices, network flow problems, and PageRank computations. Storing and operating on all <code>N*N</code> entries of such a matrix is wasteful; instead, only the non-zero entries are stored and processed.</p>
<p>SpMV computes the product <code>y=A*x</code>, where <code>A</code> is a sparse matrix, <code>x</code> is a dense input vector, and <code>y</code> is a dense output vector. For each row <code>i</code> of <code>A</code>, the result is the dot product of that row with <code>x:</code> </p>
<div class="fragment"><div class="line">y[i] = sum of A[i][j] * x[j] <span class="keywordflow">for</span> all non-zero j in row i</div>
</div><!-- fragment --><p>SpMV is one of the most important kernels in scientific computing. It appears in iterative linear solvers (conjugate gradient, GMRES), graph analytics (PageRank, label propagation), and machine learning (sparse neural networks, graph neural networks). Making SpMV fast on multi-core CPUs is therefore broadly valuable.</p>
<h1><a class="anchor" id="SpMVCSR"></a>
The CSR Storage Format</h1>
<p>The most widely used sparse matrix format is Compressed Sparse Row (CSR). CSR stores only the non-zero values and their column positions, organized row by row. It uses three arrays:</p>
<ul>
<li><code>values:</code> the non-zero values in row-major order</li>
<li><code>col_idx:</code> the column index of each non-zero value</li>
<li><code>row_ptr:</code> the starting position in <code>values</code> and <code>col_idx</code> for each row</li>
</ul>
<p>The number of non-zeros in row <code>i</code> is <code>row_ptr[i+1] - row_ptr[i]</code>, and the non-zeros themselves are at positions <code>row_ptr[i]</code> through <code>row_ptr[i+1]-1</code> in <code>values</code> and <code>col_idx</code>. To make this concrete, consider the following 5x5 sparse matrix:</p>
<div class="fragment"><div class="line">A = [ 3 0 0 1 0 ] row 0: two non-zeros at columns 0 and 3</div>
<div class="line"> [ 0 2 0 0 5 ] row 1: two non-zeros at columns 1 and 4</div>
<div class="line"> [ 1 0 4 0 0 ] row 2: two non-zeros at columns 0 and 2</div>
<div class="line"> [ 0 0 0 7 0 ] row 3: one non-zero at column 3</div>
<div class="line"> [ 0 6 0 0 2 ] row 4: two non-zeros at columns 1 and 4</div>
</div><!-- fragment --><p>Its CSR representation is:</p>
<div class="fragment"><div class="line">values = [ 3, 1, 2, 5, 1, 4, 7, 6, 2 ] <span class="comment">// all non-zeros, row by row</span></div>
<div class="line">col_idx = [ 0, 3, 1, 4, 0, 2, 3, 1, 4 ] <span class="comment">// column index of each non-zero</span></div>
<div class="line">row_ptr = [ 0, 2, 4, 6, 7, 9 ] <span class="comment">// row_ptr[i] = start of row i</span></div>
<div class="line"> <span class="comment">// row_ptr[5] = total non-zeros</span></div>
</div><!-- fragment --><p>To compute <code>y[2]</code> for example, the non-zeros in row 2 start at <code>row_ptr[2]=4</code> and end before <code>row_ptr[3]=6</code>. So <code>y[2] = values[4]*x[col_idx[4]] + values[5]*x[col_idx[5]] = 1*x[0] + 4*x[2]</code>. This format is efficient for SpMV because each row can be processed independently: row <code>i</code> reads from <code>x</code> at the column positions it needs and writes a single scalar to <code>y[i]</code>. There are no write conflicts between rows, making all rows safe to process in parallel.</p>
<h1><a class="anchor" id="SpMVWhyPartitionerMatters"></a>
Why Partitioner Choice Matters</h1>
<p>The key challenge of parallelizing SpMV is that rows have different numbers of non-zeros. In real sparse matrices this variation can be extreme: a web graph has a few hub pages with millions of links and millions of ordinary pages with just a handful. A matrix from an adaptive mesh may have a few boundary rows with many neighbours and many interior rows with just six.</p>
<ul>
<li>With <a class="el" href="classtf_1_1StaticPartitioner.html" title="class to construct a static partitioner for scheduling parallel algorithms">tf::StaticPartitioner</a>, each worker receives a fixed contiguous block of rows determined before execution begins. If one block happens to contain all the heavy rows, that worker does significantly more work than the others while they sit idle, and this load imbalance directly limits parallel speedup.</li>
<li>With <a class="el" href="classtf_1_1GuidedPartitioner.html" title="class to create a guided partitioner for scheduling parallel algorithms">tf::GuidedPartitioner</a> or <a class="el" href="classtf_1_1DynamicPartitioner.html" title="class to create a dynamic partitioner for scheduling parallel algorithms">tf::DynamicPartitioner</a>, chunk sizes are determined at runtime. Workers that finish their current chunk quickly pick up new chunks from a shared pool, naturally redistributing work away from threads that got lighter rows toward threads that are free.</li>
</ul>
<p>For matrices with uniform row lengths (banded matrices, regular meshes), static partitioning is perfectly fine and has lower overhead. For matrices with highly variable row lengths, guided or dynamic partitioning is the better choice.</p>
<h1><a class="anchor" id="SpMVImplementation"></a>
Implementation</h1>
<p>The complete self-contained example below constructs a small CSR matrix by hand, runs SpMV in parallel with three different partitioners, and verifies that all three produce the same result:</p>
<div class="fragment"><div class="line"><span class="preprocessor">#include <taskflow/taskflow.hpp></span></div>
<div class="line"> </div>
<div class="line"><span class="keywordtype">int</span> main() {</div>
<div class="line"> </div>
<div class="line"> <span class="comment">// ── CSR matrix ────────────────────────────────────────────────────────────</span></div>
<div class="line"> <span class="comment">//</span></div>
<div class="line"> <span class="comment">// A = [ 3 0 0 1 0 ]</span></div>
<div class="line"> <span class="comment">// [ 0 2 0 0 5 ]</span></div>
<div class="line"> <span class="comment">// [ 1 0 4 0 0 ]</span></div>
<div class="line"> <span class="comment">// [ 0 0 0 7 0 ]</span></div>
<div class="line"> <span class="comment">// [ 0 6 0 0 2 ]</span></div>
<div class="line"> <span class="comment">//</span></div>
<div class="line"> <span class="keyword">const</span> <span class="keywordtype">int</span> num_rows = 5;</div>
<div class="line"> </div>
<div class="line"> std::vector<float> values = { 3, 1, 2, 5, 1, 4, 7, 6, 2 };</div>
<div class="line"> std::vector<int> col_idx = { 0, 3, 1, 4, 0, 2, 3, 1, 4 };</div>
<div class="line"> std::vector<int> row_ptr = { 0, 2, 4, 6, 7, 9 };</div>
<div class="line"> </div>
<div class="line"> <span class="comment">// ── input and output vectors ──────────────────────────────────────────────</span></div>
<div class="line"> std::vector<float> x = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f };</div>
<div class="line"> std::vector<float> y(num_rows, 0.0f);</div>
<div class="line"> </div>
<div class="line"> <span class="comment">// ── SpMV kernel ───────────────────────────────────────────────────────────</span></div>
<div class="line"> <span class="comment">// Each sub-range covers a contiguous slice of rows. Within each slice the</span></div>
<div class="line"> <span class="comment">// kernel iterates the CSR non-zeros and accumulates the dot product into y[i].</span></div>
<div class="line"> <span class="comment">// Rows never share output elements, so y requires no synchronization.</span></div>
<div class="line"> <span class="keyword">auto</span> spmv_kernel = [&](<span class="keyword">const</span> <a class="code hl_typedef" href="namespacetf.html#a6c928ec9248757ba8276e316ef26846b">tf::IndexRange<int></a>& sub) {</div>
<div class="line"> <span class="keywordflow">for</span>(<span class="keywordtype">int</span> i = sub.begin(); i < sub.end(); i += sub.step_size()) {</div>
<div class="line"> <span class="keywordtype">float</span> sum = 0.0f;</div>
<div class="line"> <span class="keywordflow">for</span>(<span class="keywordtype">int</span> k = row_ptr[i]; k < row_ptr[i + 1]; k++) {</div>
<div class="line"> sum += values[k] * x[col_idx[k]];</div>
<div class="line"> }</div>
<div class="line"> y[i] = sum;</div>
<div class="line"> }</div>
<div class="line"> };</div>
<div class="line"> </div>
<div class="line"> <a class="code hl_typedef" href="namespacetf.html#a6c928ec9248757ba8276e316ef26846b">tf::IndexRange<int></a> range(0, num_rows, 1);</div>
<div class="line"> tf::Executor executor;</div>
<div class="line"> </div>
<div class="line"> <span class="comment">// ── Version 1: StaticPartitioner ─────────────────────────────────────────</span></div>
<div class="line"> <span class="comment">// Divides the row range into equal-sized chunks assigned to workers up</span></div>
<div class="line"> <span class="comment">// front. Optimal for uniform row lengths; may suffer from load imbalance</span></div>
<div class="line"> <span class="comment">// when row lengths vary significantly.</span></div>
<div class="line"> {</div>
<div class="line"> tf::Taskflow taskflow;</div>
<div class="line"> std::fill(y.begin(), y.end(), 0.0f);</div>
<div class="line"> taskflow.<a class="code hl_function" href="classtf_1_1FlowBuilder.html#a2582a216d54dacca2b7022ea7e89452a">for_each_by_index</a>(range, spmv_kernel, tf::StaticPartitioner());</div>
<div class="line"> executor.<a class="code hl_function" href="classtf_1_1Executor.html#a519777f5783981d534e9e53b99712069">run</a>(taskflow).wait();</div>
<div class="line"> printf(<span class="stringliteral">"StaticPartitioner: y = ["</span>);</div>
<div class="line"> <span class="keywordflow">for</span>(<span class="keywordtype">int</span> i = 0; i < num_rows; i++) printf(<span class="stringliteral">" %.1f"</span>, y[i]);</div>
<div class="line"> printf(<span class="stringliteral">" ]\n"</span>);</div>
<div class="line"> }</div>
<div class="line"> </div>
<div class="line"> <span class="comment">// ── Version 2: GuidedPartitioner ─────────────────────────────────────────</span></div>
<div class="line"> <span class="comment">// Starts with large chunks and progressively shrinks them as work is</span></div>
<div class="line"> <span class="comment">// consumed, naturally balancing load when rows have unequal lengths.</span></div>
<div class="line"> <span class="comment">// Recommended default for SpMV on irregular sparse matrices.</span></div>
<div class="line"> {</div>
<div class="line"> tf::Taskflow taskflow;</div>
<div class="line"> std::fill(y.begin(), y.end(), 0.0f);</div>
<div class="line"> taskflow.<a class="code hl_function" href="classtf_1_1FlowBuilder.html#a2582a216d54dacca2b7022ea7e89452a">for_each_by_index</a>(range, spmv_kernel, tf::GuidedPartitioner());</div>
<div class="line"> executor.<a class="code hl_function" href="classtf_1_1Executor.html#a519777f5783981d534e9e53b99712069">run</a>(taskflow).wait();</div>
<div class="line"> printf(<span class="stringliteral">"GuidedPartitioner: y = ["</span>);</div>
<div class="line"> <span class="keywordflow">for</span>(<span class="keywordtype">int</span> i = 0; i < num_rows; i++) printf(<span class="stringliteral">" %.1f"</span>, y[i]);</div>
<div class="line"> printf(<span class="stringliteral">" ]\n"</span>);</div>
<div class="line"> }</div>
<div class="line"> </div>
<div class="line"> <span class="comment">// ── Version 3: DynamicPartitioner ────────────────────────────────────────</span></div>
<div class="line"> <span class="comment">// Assigns one fixed-size chunk at a time to each worker dynamically.</span></div>
<div class="line"> <span class="comment">// Provides fine-grained load balancing at the cost of higher scheduling</span></div>
<div class="line"> <span class="comment">// overhead; most useful when row costs vary unpredictably.</span></div>
<div class="line"> {</div>
<div class="line"> tf::Taskflow taskflow;</div>
<div class="line"> std::fill(y.begin(), y.end(), 0.0f);</div>
<div class="line"> taskflow.<a class="code hl_function" href="classtf_1_1FlowBuilder.html#a2582a216d54dacca2b7022ea7e89452a">for_each_by_index</a>(range, spmv_kernel, tf::DynamicPartitioner());</div>
<div class="line"> executor.<a class="code hl_function" href="classtf_1_1Executor.html#a519777f5783981d534e9e53b99712069">run</a>(taskflow).wait();</div>
<div class="line"> printf(<span class="stringliteral">"DynamicPartitioner: y = ["</span>);</div>
<div class="line"> <span class="keywordflow">for</span>(<span class="keywordtype">int</span> i = 0; i < num_rows; i++) printf(<span class="stringliteral">" %.1f"</span>, y[i]);</div>
<div class="line"> printf(<span class="stringliteral">" ]\n"</span>);</div>
<div class="line"> }</div>
<div class="line"> </div>
<div class="line"> <span class="keywordflow">return</span> 0;</div>
<div class="line">}</div>
<div class="ttc" id="aclasstf_1_1Executor_html_a519777f5783981d534e9e53b99712069"><div class="ttname"><a href="classtf_1_1Executor.html#a519777f5783981d534e9e53b99712069">tf::Executor::run</a></div><div class="ttdeci">tf::Future< void > run(Taskflow &taskflow)</div><div class="ttdoc">runs a taskflow once</div></div>
<div class="ttc" id="aclasstf_1_1FlowBuilder_html_a2582a216d54dacca2b7022ea7e89452a"><div class="ttname"><a href="classtf_1_1FlowBuilder.html#a2582a216d54dacca2b7022ea7e89452a">tf::FlowBuilder::for_each_by_index</a></div><div class="ttdeci">Task for_each_by_index(R range, C callable, P part=P())</div><div class="ttdoc">constructs a parallel-for task over a one- or multi-dimensional index range</div></div>
<div class="ttc" id="anamespacetf_html_a6c928ec9248757ba8276e316ef26846b"><div class="ttname"><a href="namespacetf.html#a6c928ec9248757ba8276e316ef26846b">tf::IndexRange</a></div><div class="ttdeci">IndexRanges< T, 1 > IndexRange</div><div class="ttdoc">alias for the common 1D case of tf::IndexRanges</div><div class="ttdef"><b>Definition</b> iterator.hpp:971</div></div>
</div><!-- fragment --><p>The expected output for <code>x = {1, 2, 3, 4, 5}</code> is:</p>
<div class="fragment"><div class="line">StaticPartitioner: y = [ 7.0 12.0 13.0 28.0 22.0 ]</div>
<div class="line">GuidedPartitioner: y = [ 7.0 12.0 13.0 28.0 22.0 ]</div>
<div class="line">DynamicPartitioner: y = [ 7.0 12.0 13.0 28.0 22.0 ]</div>
</div><!-- fragment --><p>All three partitioners produce identical results; the partitioner only affects how rows are distributed among workers, not the computation itself. You can verify the first row manually: <code>y[0] = 3*x[0] + 1*x[3] = 3*1 + 1*4 = 7</code>.</p>
<h1><a class="anchor" id="SpMVDesignPoints"></a>
Design Points</h1>
<p>There are a few important design points worth noting for this example, which also apply generally to parallel sparse linear algebra algorithms:</p>
<ul>
<li>No write conflicts between rows: Each row <code>i</code> writes exclusively to <code>y[i]</code>. Because two rows never write to the same output element, no atomic operations or locks are needed on <code>y</code>, regardless of which rows are processed simultaneously.</li>
<li>Read-only shared access to <code>x:</code> Multiple workers read from <code>x</code> concurrently but never write to it during the sweep. Concurrent reads from a <code>std::vector</code> are safe without synchronization.</li>
<li>Partitioner recommendation: For matrices with uniform sparsity (regular grids, banded systems), <a class="el" href="classtf_1_1StaticPartitioner.html" title="class to construct a static partitioner for scheduling parallel algorithms">tf::StaticPartitioner</a> has the lowest overhead and is the right choice. For matrices with highly variable row lengths (power-law graphs, adaptive meshes, unstructured finite element problems), <a class="el" href="classtf_1_1GuidedPartitioner.html" title="class to create a guided partitioner for scheduling parallel algorithms">tf::GuidedPartitioner</a> delivers better load balance with modest overhead. <a class="el" href="classtf_1_1DynamicPartitioner.html" title="class to create a dynamic partitioner for scheduling parallel algorithms">tf::DynamicPartitioner</a> provides the finest-grained balancing but incurs more scheduling overhead per chunk; it is most useful when individual row costs vary unpredictably and the guided heuristic undershoots.</li>
</ul>
<dl class="section note"><dt>Note</dt><dd>The CSR format stores non-zeros in row-major order, so the inner loop over <code>k</code> accesses <code>values</code> and <code>col_idx</code> sequentially, which is cache-friendly within a single row. However, the gather access <code>x[col_idx[k]]</code> is irregular since column indices can point anywhere in <code>x</code>, and this is typically the memory bandwidth bottleneck of SpMV on large sparse matrices. </dd></dl>
</div></div><!-- contents -->
</div><!-- PageDoc -->
</div><!-- doc-content -->
<!-- HTML footer for doxygen 1.13.1-->
<!-- start footer part -->
<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
<ul>
<li class="navelem"><a class="el" href="Examples.html">Learning from Examples</a></li>
<li class="footer">
Maintained by <a href="https://tsung-wei-huang.github.io/">Dr. Tsung-Wei Huang</a>
—
Generated by <a href="https://www.doxygen.org/index.html"><img class="footer" src="doxygen.svg" width="104" height="31" alt="doxygen"/></a> 1.13.1
</li>
</ul>
</div>