<?xml version="1.0" encoding="utf-8"?> 
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">
    <generator uri="https://gohugo.io/" version="0.141.0">Hugo</generator><title type="html"><![CDATA[Cuda.compute on Blog]]></title>
    
    
    
            <link href="https://blog.scientific-python.org/tags/cuda.compute/" rel="alternate" type="text/html" title="html" />
            <link href="https://blog.scientific-python.org/tags/cuda.compute/atom.xml" rel="self" type="application/atom" title="atom" />
    <updated>2026-08-15T11:34:12+00:00</updated>
    
    
    
    
        <id>https://blog.scientific-python.org/tags/cuda.compute/</id>
    
        
        <entry>
            <title type="html"><![CDATA[Rewriting Awkward Array's GPU kernels in Python with NVIDIA's cuda.compute]]></title>
            <link href="https://blog.scientific-python.org/awkward/rewriting-gpu-kernels-cuda-compute/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>https://blog.scientific-python.org/awkward/rewriting-gpu-kernels-cuda-compute/</id>
            
            
            <published>2026-08-14T00:00:00+00:00</published>
            <updated>2026-08-14T00:00:00+00:00</updated>
            
            
            <content type="html"><![CDATA[<blockquote>How the Awkward Array and NVIDIA teams replaced thousands of lines of hand-written CUDA C++ with Python built on cuda.compute — ending up with less code that runs faster.</blockquote><p><em>Thousands of lines of hand-written CUDA C++, now Python. Less code, and it runs faster.</em></p>
<p>A single collision event in a particle detector holds a variable number of particles, each with a variable number of measurements:</p>

<div class="highlight">
  <pre>[[1.1, 2.2, 3.3], [], [4.4, 5.5]]</pre>
</div>

<p><a href="https://awkward-array.org/">Awkward Array</a> is a Python library for manipulating nested, variable-length (&ldquo;ragged&rdquo;) data like this with NumPy-like idioms. It stores that data flat, as one <code>content</code> buffer holding every value contiguously plus an <code>offsets</code> array marking where each sublist begins and ends:</p>

<div class="highlight">
  <pre>content:  [1.1, 2.2, 3.3, 4.4, 5.5]
offsets:  [0, 3, 3, 5]        # the empty middle list spans no elements</pre>
</div>

<p>Nothing is wasted on padding, but <em>every</em> operation must then be written in terms of those two buffers rather than a simple shape. That is where dense GPU tensor frameworks stop helping: they want rectangles. For several years, Awkward&rsquo;s answer was a dictionary of hand-written CUDA C++ kernels compiled at runtime with CuPy.</p>
<p>The Awkward Array and NVIDIA teams have now rebuilt that layer on <a href="https://nvidia.github.io/cccl/unstable/python/compute/index.html"><code>cuda.compute</code></a>, which brings the CUDA C++ parallel-algorithm libraries CUB and Thrust (the reductions, scans, and sorts that power production GPU software) into Python as ordinary callables. Instead of writing a kernel, the backend now composes ones that already exist. Four results stand out.</p>
<h2 id="1-the-gpu-code-is-python-now">1. The GPU code is Python now<a class="headerlink" href="#1-the-gpu-code-is-python-now" title="Link to this heading">#</a></h2>
<p><code>ak.min</code>, the minimum over each ragged sublist, took <em>three</em> kernel launches in CUDA C++: initialize a scratch buffer, reduce within each block using shared memory and explicit thread synchronization, and copy the result out. Each communicated with the next through global memory.</p>
<p>It is now a single call to a library primitive:</p>


<div class="highlight">
  <pre class="chroma"><code><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">cupy</span> <span class="k">as</span> <span class="nn">cp</span><span class="o">,</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">cuda.compute</span> <span class="kn">import</span> <span class="n">OpKind</span><span class="p">,</span> <span class="n">segmented_reduce</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">awkward_reduce_min</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">toptr</span><span class="p">:</span> <span class="n">cp</span><span class="o">.</span><span class="n">ndarray</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fromptr</span><span class="p">:</span> <span class="n">cp</span><span class="o">.</span><span class="n">ndarray</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">offsets</span><span class="p">:</span> <span class="n">cp</span><span class="o">.</span><span class="n">ndarray</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">outlength</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">identity</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">toptr</span><span class="p">[:</span><span class="n">outlength</span><span class="p">]</span> <span class="o">=</span> <span class="n">identity</span>
</span></span><span class="line"><span class="cl">    <span class="n">segmented_reduce</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">fromptr</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">toptr</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">offsets</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="n">offsets</span><span class="p">[</span><span class="mi">1</span><span class="p">:],</span>
</span></span><span class="line"><span class="cl">        <span class="n">OpKind</span><span class="o">.</span><span class="n">MINIMUM</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">np</span><span class="o">.</span><span class="n">asarray</span><span class="p">(</span><span class="n">identity</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">fromptr</span><span class="o">.</span><span class="n">dtype</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">outlength</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span></span></span></code></pre>
</div>
<p><code>segmented_reduce</code> performs one reduction per segment, and <code>offsets[:-1]</code> and <code>offsets[1:]</code> hand it the start and end of each sublist directly. No CUDA C++, no synchronization, no scratch buffers: the library handles all of it.</p>
<p>Awkward&rsquo;s user-facing API is unchanged. Underneath, 2.10.0 routes <strong>106 of its 133 GPU kernels (80%)</strong> through <code>cuda.compute</code>, up from none in 2.8.11. Every reduction and the sort now run through it with no hand-written implementation remaining. The 27 still in CUDA C++ handle structural work like jagged indexing, padding, and validity checks. Their control flow depends on the ragged layout itself, which fits the segmented primitives less naturally. That migration is ongoing. Counting only code that must actually be maintained, the switch to <code>cuda.compute</code> has so far cut hand-written CUDA C++ from <strong>8,288 lines to 2,170 — a 74% reduction</strong>.</p>
<h2 id="2-the-abstraction-made-it-faster">2. The abstraction made it faster<a class="headerlink" href="#2-the-abstraction-made-it-faster" title="Link to this heading">#</a></h2>
<p>We might expect to pay something for the abstraction.</p>
<p>Over 5,000,000 ragged sublists, <code>ak.argmin</code> takes <strong>0.96 ms per call</strong> as a hand-written CUDA kernel and <strong>0.39 ms</strong> through <code>cuda.compute</code>: <strong>2.5x faster, with identical output</strong>.</p>
<p>That speedup is inherited rather than hand-tuned, which is exactly the point of building on CUB: segmented reductions are difficult to write well by hand, and CUB&rsquo;s have been tuned per architecture for years. Awkward gets that tuning now, and the next architecture&rsquo;s when it ships, without changing its own code.</p>
<h2 id="3-a-whole-physics-formula-can-collapse-into-one-kernel">3. A whole physics formula can collapse into one kernel<a class="headerlink" href="#3-a-whole-physics-formula-can-collapse-into-one-kernel" title="Link to this heading">#</a></h2>
<p>The first two results came from the migration. For this one, a user drops down to <code>cuda.compute</code> and composes the primitives by hand.</p>
<p>Awkward evaluates eagerly: every operation returns a real array, so a chain of them writes an intermediate to global memory at each step and reads it back at the next. <code>cuda.compute</code> algorithms instead accept <strong>iterators</strong> that are evaluated lazily as the algorithm runs, letting many logical steps ride along inside a single pass. That is <a href="https://developer.nvidia.com/blog/kernel-fusion-in-nvidia-cuda-optimizing-memory-traffic-and-launch-overhead/">kernel fusion</a>, and it saves both the memory traffic and the launch overhead.</p>
<h3 id="example-di-muon-invariant-mass">Example: di-muon invariant mass<a class="headerlink" href="#example-di-muon-invariant-mass" title="Link to this heading">#</a></h3>
<p>The opposite-sign di-muon invariant mass, a standard reconstruction in particle physics, combines a few measured quantities for every pair of particles in an event. In Awkward, it is one line:</p>


<div class="highlight">
  <pre class="chroma"><code><span class="line"><span class="cl"><span class="n">mu1</span><span class="p">,</span> <span class="n">mu2</span> <span class="o">=</span> <span class="n">ak</span><span class="o">.</span><span class="n">unzip</span><span class="p">(</span><span class="n">ak</span><span class="o">.</span><span class="n">combinations</span><span class="p">(</span><span class="n">muons</span><span class="p">,</span> <span class="mi">2</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">mass</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="mi">2</span> <span class="o">*</span> <span class="n">mu1</span><span class="o">.</span><span class="n">pt</span> <span class="o">*</span> <span class="n">mu2</span><span class="o">.</span><span class="n">pt</span> <span class="o">*</span> <span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">cosh</span><span class="p">(</span><span class="n">mu1</span><span class="o">.</span><span class="n">eta</span> <span class="o">-</span> <span class="n">mu2</span><span class="o">.</span><span class="n">eta</span><span class="p">)</span> <span class="o">-</span> <span class="n">np</span><span class="o">.</span><span class="n">cos</span><span class="p">(</span><span class="n">mu1</span><span class="o">.</span><span class="n">phi</span> <span class="o">-</span> <span class="n">mu2</span><span class="o">.</span><span class="n">phi</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre>
</div>
<p>Evaluated step by step, that chain of arithmetic and trigonometric operations becomes one or more kernels per step, with every intermediate written out as a full-length array and read back.</p>
<p>Written by hand as one <code>cuda.compute</code> call, the whole formula becomes a single operator: a <code>gpu_struct</code> keeps each particle&rsquo;s fields together, a <code>ZipIterator</code> combines them, and a <code>PermutationIterator</code> produces each pair on demand. The operator sees one complete pair at a time, and no intermediate is ever built.</p>
<p>Over all such pairs in a CMS open-data sample:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th><strong>kernel launches</strong></th>
          <th><strong>memory operations</strong></th>
          <th><strong>GPU time</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>step by step</td>
          <td>88</td>
          <td>212</td>
          <td>25.7 ms</td>
      </tr>
      <tr>
          <td>fused into one call</td>
          <td><strong>1</strong></td>
          <td>45</td>
          <td><strong>10.2 ms</strong></td>
      </tr>
  </tbody>
</table>
<p>2.5x faster, with nothing allocated in between.</p>
<h2 id="4-at-analysis-scale-the-gap-widens">4. At analysis scale, the gap widens<a class="headerlink" href="#4-at-analysis-scale-the-gap-widens" title="Link to this heading">#</a></h2>
<p>We ran the <a href="https://github.com/CoffeaTeam/coffea-benchmarks">ADL benchmark queries</a> (a standard set of physics-analysis tasks) on CMS 2012 open data, against released Awkward 2.8.11 on its hand-written CuPy backend, the last version before <code>cuda.compute</code>. For the two combinatoric queries below, the Awkward expression is <strong>unchanged</strong>; only the backend underneath differs, so the comparison isolates the migration itself.</p>
<p>Measured on the <strong>GPU compute stage</strong> at 100k, 1M, and 10M events, both speed up by margins that grow with the data:</p>
<ul>
<li>the di-muon reconstruction from section 3: <strong>60x → 422x → 3634x</strong></li>
<li>a second combinatoric query: <strong>45x rising to 250x</strong></li>
</ul>
<p><code>cuda.compute</code>&rsquo;s time stays approximately constant across those sizes while the hand-written implementation grows super-linearly, so the gap widens as data grows rather than closing.</p>
<h2 id="the-result-that-isnt-a-number">The result that isn&rsquo;t a number<a class="headerlink" href="#the-result-that-isnt-a-number" title="Link to this heading">#</a></h2>
<p>A stated goal of the Awkward Array project is to let physicists and data analysts write high-performance code in Python without GPU expertise. The old backend required contributors to understand CUDA thread hierarchies, atomics, and shared-memory behavior before they could add or fix a kernel. The new one asks for an ordinary Python function and a call to the right primitive. Domain scientists can read it, review it, and unit-test its logic without a GPU.</p>
<p>Awkward knows the problem. <code>cuda.compute</code> knows the hardware. The result is Python that&rsquo;s simpler and faster than the CUDA C++ it replaced.</p>
<p><em>Full methodology, per-query results, and the code-counting rules are in &ldquo;GPU-Accelerated Awkward Arrays with CUDA Python&rdquo; by Ashwin Srinath (NVIDIA) and Ianna Osborne (Princeton University), Proceedings of the 24th Python in Science Conference (SciPy 2026). All measurements were taken on an NVIDIA RTX PRO 6000 Blackwell Server Edition with CUDA 13.2, <code>cuda.compute</code> 1.1.0, and CuPy 14.1.1; every benchmark was run twice on independent machines, with structural counts identical and timings agreeing to within a few percent. Migration progress is tracked in <a href="https://github.com/scikit-hep/awkward/issues/3793">scikit-hep/awkward#3793</a>.</em></p>
<p><em>Much of the kernel migration was implemented by Maxym Naumchyk. Thanks also to the <code>cuda.compute</code> and CUB/Thrust developers at NVIDIA and to the Scikit-HEP community. This work was supported in part by NSF grants OAC-1450377, OAC-1836650, OAC-2103945, PHY-2121686, and PHY-2323298.</em></p>
]]></content>
            
                 
                    
                 
                    
                         
                        
                            
                             
                                <category scheme="taxonomy:Tags" term="awkward-array" label="Awkward Array" />
                             
                                <category scheme="taxonomy:Tags" term="gpu" label="GPU" />
                             
                                <category scheme="taxonomy:Tags" term="cuda" label="CUDA" />
                             
                                <category scheme="taxonomy:Tags" term="cuda.compute" label="cuda.compute" />
                             
                                <category scheme="taxonomy:Tags" term="scikit-hep" label="Scikit-HEP" />
                            
                        
                    
                
            
        </entry>
    
</feed>
