<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-08-19T13:01:08+00:00</updated><id>/feed.xml</id><title type="html">Blog of Adrian Scheerer</title><subtitle>Some ideas I have come across recently</subtitle><entry><title type="html">Fast vector addition on A100 - 80 GB</title><link href="/2026/06/19/fast-vector-addition-A100.html" rel="alternate" type="text/html" title="Fast vector addition on A100 - 80 GB" /><published>2026-06-19T10:07:03+00:00</published><updated>2026-06-19T10:07:03+00:00</updated><id>/2026/06/19/fast-vector-addition-A100</id><content type="html" xml:base="/2026/06/19/fast-vector-addition-A100.html"><![CDATA[<p><a href="https://github.com/adrische/learn-cuda/blob/main/Own-Kernels/GPUMODE/submission.py">This</a> is my solution to the float16 vector addition problem <a href="">https://www.gpumode.com/leaderboard/543?tab=rankings</a></p>

<p>At time of writing I’m listed at 6th place (tied with places 6-9), and I have run out of ideas what to try. The currently best solution is 0.27% (2.4 μs) faster.</p>

<p>My solution is based on interpreting 8 float16 as 4 float32 (or one float4, to be precise) to saturate memory bandwidth, doing the calculations using the half2 vector type to maximize arithmetic efficiency, and finding the <a href="https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/cpp-language-extensions.html#low-level-load-and-store-functions">fastest load / store operations</a>, and the finding best launch configuration.</p>

<h4 id="some-observations">Some observations</h4>

<p>Sometimes I would get different times for the same kernel, e.g., 958 μs and 895μs for the exact same submission, but I could not figure out why.</p>

<p>The relative performance between different kernels may not be consistent between different GPUs. I had access to an A100 - 40 GB, and what was fastest for this GPU was not necessarily fastest for the benchmarked A100 - 80 GB (mainly the launch configuration).</p>

<p>There was an inconsistency how nvcc would compile a local cu file, vs how load inline would compile inline cuda code. I tried to make the two methods consistent by including the following code in the submission:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">torch.utils.cpp_extension</span> <span class="kn">import</span> <span class="n">COMMON_NVCC_FLAGS</span>

<span class="n">flags_to_remove</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s">'-D__CUDA_NO_HALF_OPERATORS__'</span><span class="p">,</span>
    <span class="s">'-D__CUDA_NO_HALF_CONVERSIONS__'</span><span class="p">,</span>
    <span class="s">'-D__CUDA_NO_BFLOAT16_CONVERSIONS__'</span><span class="p">,</span>
    <span class="s">'-D__CUDA_NO_HALF2_OPERATORS__'</span><span class="p">,</span>
    <span class="s">'--expt-relaxed-constexpr'</span>
<span class="p">]</span>

<span class="k">for</span> <span class="n">flag</span> <span class="ow">in</span> <span class="n">flags_to_remove</span><span class="p">:</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">COMMON_NVCC_FLAGS</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">flag</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">ValueError</span><span class="p">:</span>
        <span class="k">pass</span>  <span class="c1"># Flag was already absent
</span>
<span class="n">add_module</span> <span class="o">=</span> <span class="n">load_inline</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">'add_cudafloat4'</span><span class="p">,</span>
    <span class="n">cpp_sources</span><span class="o">=</span><span class="n">add_cpp_source</span><span class="p">,</span>
    <span class="n">cuda_sources</span><span class="o">=</span><span class="n">add_cuda_source</span><span class="p">,</span>
    <span class="n">functions</span><span class="o">=</span><span class="p">[</span><span class="s">'add_cuda'</span><span class="p">],</span>
    <span class="n">verbose</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">extra_cuda_cflags</span><span class="o">=</span><span class="p">[</span><span class="s">"-gencode=arch=compute_80,code=sm_80"</span><span class="p">,</span> <span class="s">'--use_fast_math'</span><span class="p">,</span> <span class="s">'-O3'</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<h4 id="what-did-not-work-for-me">What did not work (for me):</h4>

<p>A custom struct consisting of 8 half numbers, for a total of 128 bit. The compiler would not generate v4 load or store instructions.</p>

<p>Doing part of the calculation on the CPU:</p>
<ul>
  <li>The idea is that in principle one could transfer a very small part of the data back to the CPU, while the GPU calculates, then doing the vector addition for that small part of the data on the CPU, and transferring the data back. In principle this could save a fraction of host-device-speed/global-memory-speed of overall execution time.</li>
  <li>However, transferring part of the data asyncronously to the CPU requires streams, and stream creation overhead is &gt; 10ms, much too large for this small kernel.</li>
  <li>Additionally, the calculation is then CPU compute bound -&gt; further reduction in possible benefit.</li>
</ul>

<p>Trying custom <a href="https://docs.nvidia.com/cuda/parallel-thread-execution/index.html#data-movement-and-conversion-instructions-st">asm load and store operations</a> did not give better performance.
For example,</p>
<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__stwt</span><span class="p">(</span><span class="o">&amp;</span><span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="n">float4</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">C</span><span class="p">)[</span><span class="n">i</span><span class="p">],</span> <span class="n">c</span><span class="p">);</span>
</code></pre></div></div>
<p>could be accessed as</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">asm</span><span class="p">(</span><span class="s">"st.global.wt.v4.f32 	[%0], {%1, %2, %3, %4};"</span>
<span class="o">:</span> 
<span class="o">:</span> <span class="s">"l"</span><span class="p">(</span><span class="o">&amp;</span><span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="n">float4</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">C</span><span class="p">)[</span><span class="n">i</span><span class="p">]),</span> <span class="s">"f"</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">x</span><span class="p">),</span> <span class="s">"f"</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">y</span><span class="p">),</span> <span class="s">"f"</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">z</span><span class="p">),</span><span class="s">"f"</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">w</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p>and then be replaced by a more specific operation.</p>

<h4 id="what-turned-out-to-be-not-relevant">What turned out to be not relevant:</h4>

<p>Interestingly, the solution does not use any features that would be specific to Ampere or compute capability 8.0.</p>

<p>Size-specific kernels / launch configurations, e.g., changing the number of blocks for smaller problems. The leaderboard was based on the performance for only one problem. Otherwise one could try to profile / ncu all problem sizes individually - I briefly tried this, and the same kernel and same launch specification would show very different characteristics for different problem sizes (e.g., memory utilization much lower).</p>

<h4 id="resources">Resources</h4>

<p>Code for some other leaderboard entries:</p>
<ul>
  <li><a href="">https://github.com/NitishNaineni/gpumode/tree/master/pmpp_v2/vectoradd_py</a></li>
  <li><a href="">https://github.com/CaptnJackSparrow/reference-kernels/blob/20260209/problems/pmpp_v2/vectoradd_py/solutions/correct/submission_cuda_inline_A100.py</a></li>
</ul>

<p>Some interesting related papers:</p>
<ul>
  <li><a href="https://www.nvidia.com/content/GTC-2010/pdfs/2238_GTC2010.pdf">Better Performance at Lower Occupancy</a> (instruction-level parallelism)</li>
  <li><a href="https://arxiv.org/pdf/2208.11174">Demystifying the Nvidia Ampere Architecture through Microbenchmarking and Instruction-level Analysis</a> (analysis of cycles for selected instructions, and memory latency benchmarking)</li>
  <li><a href="https://images.nvidia.com/aem-dam/en-zz/Solutions/data-center/nvidia-ampere-architecture-whitepaper.pdf">Ampere architecture whitepaper</a></li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[This is my solution to the float16 vector addition problem https://www.gpumode.com/leaderboard/543?tab=rankings]]></summary></entry><entry><title type="html">Older posts</title><link href="/2026/01/01/older-posts.html" rel="alternate" type="text/html" title="Older posts" /><published>2026-01-01T10:07:03+00:00</published><updated>2026-01-01T10:07:03+00:00</updated><id>/2026/01/01/older-posts</id><content type="html" xml:base="/2026/01/01/older-posts.html"><![CDATA[<p>Older posts are available on my <a href="https://github.com/adrische/write-ups">write-ups</a> repository.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Older posts are available on my write-ups repository.]]></summary></entry></feed>