<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.haidhi.my.id/wiki/index.php?action=history&amp;feed=atom&amp;title=Verify_FAILED_in_multi-rank_runs</id>
	<title>Verify FAILED in multi-rank runs - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.haidhi.my.id/wiki/index.php?action=history&amp;feed=atom&amp;title=Verify_FAILED_in_multi-rank_runs"/>
	<link rel="alternate" type="text/html" href="https://wiki.haidhi.my.id/wiki/index.php?title=Verify_FAILED_in_multi-rank_runs&amp;action=history"/>
	<updated>2026-11-01T19:56:21Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.0</generator>
	<entry>
		<id>https://wiki.haidhi.my.id/wiki/index.php?title=Verify_FAILED_in_multi-rank_runs&amp;diff=706&amp;oldid=prev</id>
		<title>SanzSkuy: Created page with &quot;This page documents a correctness bug in the MPI × OpenCL matmul cluster where multi-rank runs produced zeroed output elements, the investigation that isolated it, and the fix. It also records three secondary issues found along the way.  &#039;&#039;&#039;TL;DR&#039;&#039;&#039;: The OpenCL kernel maps NDRange dimension&amp;nbsp;0 to the matrix &#039;&#039;row&#039;&#039; and dimension&amp;nbsp;1 to the &#039;&#039;column&#039;&#039;, but the host launch code set the global work size as &lt;code&gt;{cols, rows}&lt;/code&gt; — the two were swapped. Square m...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.haidhi.my.id/wiki/index.php?title=Verify_FAILED_in_multi-rank_runs&amp;diff=706&amp;oldid=prev"/>
		<updated>2026-06-28T04:04:58Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;This page documents a correctness bug in the MPI × OpenCL matmul cluster where multi-rank runs produced zeroed output elements, the investigation that isolated it, and the fix. It also records three secondary issues found along the way.  &amp;#039;&amp;#039;&amp;#039;TL;DR&amp;#039;&amp;#039;&amp;#039;: The OpenCL kernel maps NDRange dimension 0 to the matrix &amp;#039;&amp;#039;row&amp;#039;&amp;#039; and dimension 1 to the &amp;#039;&amp;#039;column&amp;#039;&amp;#039;, but the host launch code set the global work size as &amp;lt;code&amp;gt;{cols, rows}&amp;lt;/code&amp;gt; — the two were swapped. Square m...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;This page documents a correctness bug in the MPI × OpenCL matmul cluster where&lt;br /&gt;
multi-rank runs produced zeroed output elements, the investigation that isolated&lt;br /&gt;
it, and the fix. It also records three secondary issues found along the way.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;TL;DR&amp;#039;&amp;#039;&amp;#039;: The OpenCL kernel maps NDRange dimension&amp;amp;nbsp;0 to the matrix &amp;#039;&amp;#039;row&amp;#039;&amp;#039;&lt;br /&gt;
and dimension&amp;amp;nbsp;1 to the &amp;#039;&amp;#039;column&amp;#039;&amp;#039;, but the host launch code set the global&lt;br /&gt;
work size as &amp;lt;code&amp;gt;{cols, rows}&amp;lt;/code&amp;gt; — the two were swapped. Square matrices hid&lt;br /&gt;
the bug because the dimensions were equal; any non-square per-rank slice left&lt;br /&gt;
whole rows/columns with no work-item, so they stayed zero.&lt;br /&gt;
&lt;br /&gt;
== Symptom ==&lt;br /&gt;
&lt;br /&gt;
Single-device runs verified correctly, but every multi-rank run failed the&lt;br /&gt;
built-in spot-check:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
rank  host          type  device                                    rows   kernel_ms    gflop/s&lt;br /&gt;
0     darmogandolz  GPU   NVIDIA GeForce RTX 3050 6GB Laptop GPU      86        0.44      25.41&lt;br /&gt;
1     tumbal        CPU   cpu-ivybridge-AMD A8-6410 APU with AMD      85       13.28       0.84&lt;br /&gt;
2     localhost     GPU   QUALCOMM Adreno(TM)                         85       10.13       1.10&lt;br /&gt;
&lt;br /&gt;
wall 0.060 s  ·  aggregate 0.55 GFLOP/s  ·  verify FAILED at C[194][106] gpu=0.00 cpu=7256.00&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Key observations:&lt;br /&gt;
* The failing value was exactly &amp;lt;code&amp;gt;0.00&amp;lt;/code&amp;gt; — an output element that was never written, not a rounding error.&lt;br /&gt;
* It happened on every device type (NVIDIA GPU, AMD CPU, Adreno GPU), so it was not device-specific.&lt;br /&gt;
* Single-device square runs (e.g. 256×256×256) always passed.&lt;br /&gt;
&lt;br /&gt;
== Investigation ==&lt;br /&gt;
&lt;br /&gt;
=== Step 1 — Square vs. non-square ===&lt;br /&gt;
&lt;br /&gt;
A 3-node cluster run was repeated while varying only &amp;lt;code&amp;gt;A_rows&amp;lt;/code&amp;gt; so that&lt;br /&gt;
the per-rank row slice changed shape:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! &amp;lt;code&amp;gt;A_rows&amp;lt;/code&amp;gt; !! Per-rank slice !! Result&lt;br /&gt;
|-&lt;br /&gt;
| 768 || 256 rows (&amp;#039;&amp;#039;square&amp;#039;&amp;#039;: rows = cols) || &amp;lt;span style=&amp;quot;color:green&amp;quot;&amp;gt;&amp;#039;&amp;#039;verify OK&amp;#039;&amp;#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 300 || 100 rows (non-square) || &amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;&amp;#039;&amp;#039;verify FAILED&amp;#039;&amp;#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 255 || 85 rows (non-square) || &amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;&amp;#039;&amp;#039;verify FAILED&amp;#039;&amp;#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
The discriminator was clear: &amp;#039;&amp;#039;&amp;#039;the bug appears only when a rank&amp;#039;s slice is&lt;br /&gt;
non-square&amp;#039;&amp;#039;&amp;#039; (&amp;lt;code&amp;gt;my_rows ≠ B_cols&amp;lt;/code&amp;gt;). Because matmul distributes &amp;#039;&amp;#039;rows&amp;#039;&amp;#039;&lt;br /&gt;
across ranks while every rank keeps the full &amp;lt;code&amp;gt;B_cols&amp;lt;/code&amp;gt;, every&lt;br /&gt;
multi-rank run produces non-square slices — so every multi-rank run failed.&lt;br /&gt;
&lt;br /&gt;
=== Step 2 — Misleading kernel source file ===&lt;br /&gt;
&lt;br /&gt;
The repository still contained &amp;lt;code&amp;gt;matmul/kernels/matmul.cl&amp;lt;/code&amp;gt; (marked&lt;br /&gt;
deleted in &amp;lt;code&amp;gt;git status&amp;lt;/code&amp;gt;) whose index convention was:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
int col = get_global_id(0);   // dim 0 = column&lt;br /&gt;
int row = get_global_id(1);   // dim 1 = row&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An earlier &amp;quot;fix&amp;quot; had been based on &amp;#039;&amp;#039;&amp;#039;this stale file&amp;#039;&amp;#039;&amp;#039;. The kernel actually&lt;br /&gt;
compiled into the binary was embedded as a string literal in&lt;br /&gt;
&amp;lt;code&amp;gt;matmul/src/cl_runner.cpp&amp;lt;/code&amp;gt;, with the &amp;#039;&amp;#039;&amp;#039;opposite&amp;#039;&amp;#039;&amp;#039; convention:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;c&amp;quot;&amp;gt;&lt;br /&gt;
int row = get_global_id(0);   // dim 0 = ROW&lt;br /&gt;
int col = get_global_id(1);   // dim 1 = COL&lt;br /&gt;
if (row &amp;gt;= rowsA || col &amp;gt;= N) return;&lt;br /&gt;
C[row * N + col] = acc;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Root cause ==&lt;br /&gt;
&lt;br /&gt;
The embedded kernel expects the global work size to be &amp;lt;code&amp;gt;{rows, cols}&amp;lt;/code&amp;gt;,&lt;br /&gt;
but the host set it to &amp;lt;code&amp;gt;{cols, rows}&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;cpp&amp;quot;&amp;gt;&lt;br /&gt;
// WRONG — dimensions swapped relative to the kernel&lt;br /&gt;
size_t global[2] = { (size_t)B_cols, (size_t)my_rows };&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Effect for a non-square slice &amp;lt;code&amp;gt;my_rows = 100&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;B_cols = 256&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
* Dimension&amp;amp;nbsp;0 ranged over &amp;lt;code&amp;gt;[0, 256)&amp;lt;/code&amp;gt; but was used as the &amp;#039;&amp;#039;row&amp;#039;&amp;#039; index. The bounds check &amp;lt;code&amp;gt;row &amp;gt;= rowsA&amp;lt;/code&amp;gt; discarded rows ≥ 100.&lt;br /&gt;
* Dimension&amp;amp;nbsp;1 ranged over &amp;lt;code&amp;gt;[0, 100)&amp;lt;/code&amp;gt; but was used as the &amp;#039;&amp;#039;column&amp;#039;&amp;#039; index. Columns &amp;lt;code&amp;gt;[100, 256)&amp;lt;/code&amp;gt; &amp;#039;&amp;#039;&amp;#039;never received a work-item&amp;#039;&amp;#039;&amp;#039;, so those elements of &amp;lt;code&amp;gt;C&amp;lt;/code&amp;gt; kept their initial value of zero.&lt;br /&gt;
&lt;br /&gt;
When the slice was square (&amp;lt;code&amp;gt;my_rows == B_cols&amp;lt;/code&amp;gt;) the two ranges were&lt;br /&gt;
identical, so the swap had no visible effect — which is why square runs always&lt;br /&gt;
passed and the bug hid for so long.&lt;br /&gt;
&lt;br /&gt;
=== Why verify was sometimes a false negative ===&lt;br /&gt;
&lt;br /&gt;
The spot-check only samples 8 elements of &amp;lt;code&amp;gt;C&amp;lt;/code&amp;gt;. On some shapes none of&lt;br /&gt;
the sampled coordinates landed in the zeroed region, so the run reported&lt;br /&gt;
&amp;lt;code&amp;gt;verify OK&amp;lt;/code&amp;gt; even though part of the output was wrong. This made the&lt;br /&gt;
bug look intermittent until the row/column sweep pinned it down.&lt;br /&gt;
&lt;br /&gt;
== Fix ==&lt;br /&gt;
&lt;br /&gt;
In &amp;lt;code&amp;gt;matmul/src/cl_runner.cpp&amp;lt;/code&amp;gt;, set the global work size in the order&lt;br /&gt;
the kernel expects, use an explicit local work-group size, and round each&lt;br /&gt;
dimension &amp;#039;&amp;#039;&amp;#039;up&amp;#039;&amp;#039;&amp;#039; to a multiple of it. The kernel&amp;#039;s existing bounds check&lt;br /&gt;
(&amp;lt;code&amp;gt;if (row &amp;gt;= rowsA || col &amp;gt;= N) return;&amp;lt;/code&amp;gt;) discards the extra&lt;br /&gt;
work-items.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;cpp&amp;quot;&amp;gt;&lt;br /&gt;
// dim 0 = rows, dim 1 = cols — matches the kernel&amp;#039;s get_global_id() mapping&lt;br /&gt;
size_t ts = cl.max_wg &amp;gt;= 256 ? 16 : (cl.max_wg &amp;gt;= 64 ? 8 : 4);&lt;br /&gt;
size_t local[2]  = { ts, ts };&lt;br /&gt;
size_t global[2] = {&lt;br /&gt;
    ((size_t)(my_rows + ts - 1) / ts) * ts,   // dim 0 = rows&lt;br /&gt;
    ((size_t)(B_cols  + ts - 1) / ts) * ts     // dim 1 = cols&lt;br /&gt;
};&lt;br /&gt;
clEnqueueNDRangeKernel(cl.queue, cl.kernel, 2, nullptr, global, local, 0, nullptr, nullptr);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;ts&amp;lt;/code&amp;gt; is chosen from &amp;lt;code&amp;gt;CL_KERNEL_WORK_GROUP_SIZE&amp;lt;/code&amp;gt; (stored as&lt;br /&gt;
&amp;lt;code&amp;gt;max_wg&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;ClContext&amp;lt;/code&amp;gt;) so the local size is valid on&lt;br /&gt;
every device. Rounding the global size up and relying on the bounds check also&lt;br /&gt;
removes a related hazard: with a &amp;lt;code&amp;gt;NULL&amp;lt;/code&amp;gt; local size and an exact&lt;br /&gt;
global size, some drivers silently drop the remainder work-items when a&lt;br /&gt;
dimension is not divisible by the driver-chosen local size.&lt;br /&gt;
&lt;br /&gt;
=== Verification ===&lt;br /&gt;
&lt;br /&gt;
After the fix (and a clean rebuild on every node), all shapes pass on all three&lt;br /&gt;
devices, both solo and in the 3-node cluster:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Scenario !! Before !! After&lt;br /&gt;
|-&lt;br /&gt;
| Phone solo, non-square (128/85/512 rows) || &amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;&amp;#039;&amp;#039;FAILED&amp;#039;&amp;#039;&amp;lt;/span&amp;gt; || &amp;lt;span style=&amp;quot;color:green&amp;quot;&amp;gt;&amp;#039;&amp;#039;OK&amp;#039;&amp;#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Cluster &amp;lt;code&amp;gt;A_rows&amp;lt;/code&amp;gt; = 300 / 255 (non-square slices) || &amp;lt;span style=&amp;quot;color:red&amp;quot;&amp;gt;&amp;#039;&amp;#039;FAILED&amp;#039;&amp;#039;&amp;lt;/span&amp;gt; || &amp;lt;span style=&amp;quot;color:green&amp;quot;&amp;gt;&amp;#039;&amp;#039;OK&amp;#039;&amp;#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;./test.sh&amp;lt;/code&amp;gt; full suite || 6/8 || &amp;lt;span style=&amp;quot;color:green&amp;quot;&amp;gt;&amp;#039;&amp;#039;9/9&amp;#039;&amp;#039;&amp;lt;/span&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Secondary issues found ==&lt;br /&gt;
&lt;br /&gt;
These surfaced while reproducing and verifying the fix on real hardware.&lt;br /&gt;
&lt;br /&gt;
=== Stale object files on the phone ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;make deploy-android&amp;lt;/code&amp;gt; ships source via &amp;lt;code&amp;gt;tar&amp;lt;/code&amp;gt;, which&lt;br /&gt;
preserves modification times. If the phone&amp;#039;s &amp;lt;code&amp;gt;build/&amp;lt;/code&amp;gt; already held&lt;br /&gt;
newer &amp;lt;code&amp;gt;.o&amp;lt;/code&amp;gt; files, &amp;lt;code&amp;gt;make&amp;lt;/code&amp;gt; skipped recompiling the changed&lt;br /&gt;
source and the phone kept running old code. &amp;#039;&amp;#039;&amp;#039;Always force a clean rebuild on&lt;br /&gt;
the phone after a source change:&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ssh -p 8022 &amp;lt;phone&amp;gt; &amp;#039;cd matmul-src &amp;amp;&amp;amp; rm -rf build &amp;amp;&amp;amp; \&lt;br /&gt;
  cmake -B build &amp;amp;&amp;amp; cmake --build build &amp;amp;&amp;amp; cp build/matmul ~/matmul&amp;#039;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== &amp;lt;code&amp;gt;/tmp&amp;lt;/code&amp;gt; is not writable in Termux ===&lt;br /&gt;
&lt;br /&gt;
The energy/power scripts originally wrote the sampler output to&lt;br /&gt;
&amp;lt;code&amp;gt;/tmp/.watt&amp;lt;/code&amp;gt;. On Android/Termux &amp;lt;code&amp;gt;/tmp&amp;lt;/code&amp;gt; is not writable&lt;br /&gt;
(&amp;lt;code&amp;gt;Permission denied&amp;lt;/code&amp;gt;), so the file was empty. Fix: write to the Termux&lt;br /&gt;
home (&amp;lt;code&amp;gt;$ANDROID_HOME&amp;lt;/code&amp;gt;) instead.&lt;br /&gt;
&lt;br /&gt;
=== Race when stopping the power sampler ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;power_sample.sh&amp;lt;/code&amp;gt; only writes its mean on receiving &amp;lt;code&amp;gt;SIGTERM&amp;lt;/code&amp;gt;&lt;br /&gt;
(via a trap), and the trap runs only after the current &amp;lt;code&amp;gt;sleep&amp;lt;/code&amp;gt;&lt;br /&gt;
interval finishes. A fixed &amp;lt;code&amp;gt;sleep 0.3; cat&amp;lt;/code&amp;gt; after &amp;lt;code&amp;gt;kill&amp;lt;/code&amp;gt;&lt;br /&gt;
raced that write and read an empty file. Fixes:&lt;br /&gt;
* Same SSH session: &amp;lt;code&amp;gt;kill -TERM $p; wait $p&amp;lt;/code&amp;gt;, then read.&lt;br /&gt;
* Separate SSH sessions (benchmark): poll until the output file is non-empty before reading.&lt;br /&gt;
&lt;br /&gt;
Also note: &amp;lt;code&amp;gt;kill -INT&amp;lt;/code&amp;gt; on a backgrounded non-interactive bash script&lt;br /&gt;
was unreliable in this environment (the trap did not fire while &amp;lt;code&amp;gt;sleep&amp;lt;/code&amp;gt;&lt;br /&gt;
was running); &amp;lt;code&amp;gt;kill -TERM&amp;lt;/code&amp;gt; works. The sampler traps both.&lt;br /&gt;
&lt;br /&gt;
== Affected files ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Change&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;matmul/src/cl_runner.cpp&amp;lt;/code&amp;gt; || Swap global dims to &amp;lt;code&amp;gt;{rows, cols}&amp;lt;/code&amp;gt;; explicit local size + round-up; query &amp;lt;code&amp;gt;CL_KERNEL_WORK_GROUP_SIZE&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;matmul/include/cl_runner.hpp&amp;lt;/code&amp;gt; || Add &amp;lt;code&amp;gt;size_t max_wg&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;ClContext&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;power_sample.sh&amp;lt;/code&amp;gt; || Battery rail read via direct &amp;lt;code&amp;gt;cat&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;su -c&amp;lt;/code&amp;gt; fallback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;benchmark.sh&amp;lt;/code&amp;gt; || Phone power: write to &amp;lt;code&amp;gt;$ANDROID_HOME&amp;lt;/code&amp;gt;, poll output file&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;test.sh&amp;lt;/code&amp;gt; || Phone power probe: write to &amp;lt;code&amp;gt;$ANDROID_HOME&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;wait&amp;lt;/code&amp;gt; for sampler exit&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Lessons ==&lt;br /&gt;
&lt;br /&gt;
* When a GPU kernel writes zeros, suspect work-item coverage (NDRange dimension order / size) before arithmetic.&lt;br /&gt;
* Square test matrices mask dimension-order bugs — always test a non-square shape.&lt;br /&gt;
* An 8-sample spot-check can be a false negative; for a correctness bug, sweep shapes rather than trusting a single pass.&lt;br /&gt;
* Keep the kernel source in one place. A stale, deleted &amp;lt;code&amp;gt;.cl&amp;lt;/code&amp;gt; file with the opposite convention sent the first fix in the wrong direction.&lt;br /&gt;
* On Termux: &amp;lt;code&amp;gt;/tmp&amp;lt;/code&amp;gt; is not writable, and &amp;lt;code&amp;gt;tar&amp;lt;/code&amp;gt;-based deploys need a clean rebuild to avoid stale objects.&lt;/div&gt;</summary>
		<author><name>SanzSkuy</name></author>
	</entry>
</feed>