<?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="https://kredd2506.github.io/modlearn/feed.xml" rel="self" type="application/atom+xml" /><link href="https://kredd2506.github.io/modlearn/" rel="alternate" type="text/html" /><updated>2026-09-29T21:40:21+00:00</updated><id>https://kredd2506.github.io/modlearn/feed.xml</id><title type="html">Chip or Software? · MAX on Qualcomm</title><subtitle>Re-running a published Qualcomm Cloud AI 100 Ultra vs NVIDIA A100 LLM-serving benchmark with Modular&apos;s MAX and Mojo, to test whether its conclusions came from the hardware or from the software stack.
</subtitle><entry><title type="html">Testing the stopwatch: building an LLM benchmark on a MacBook before renting GPUs</title><link href="https://kredd2506.github.io/modlearn/2026/09/28/benchmarking-before-benchmarking.html" rel="alternate" type="text/html" title="Testing the stopwatch: building an LLM benchmark on a MacBook before renting GPUs" /><published>2026-09-28T00:00:00+00:00</published><updated>2026-09-28T00:00:00+00:00</updated><id>https://kredd2506.github.io/modlearn/2026/09/28/benchmarking-before-benchmarking</id><content type="html" xml:base="https://kredd2506.github.io/modlearn/2026/09/28/benchmarking-before-benchmarking.html"><![CDATA[<p>This project asks one question: <strong>was it the chip or the software?</strong></p>

<p>In 2025, Sada et al. published
<a href="https://arxiv.org/abs/2507.00418">“Serving LLMs in HPC Clusters: A Comparative Study of Qualcomm Cloud AI 100 Ultra and NVIDIA Data Center GPUs”</a>.
They served 12 models with vLLM on both platforms and found that the Qualcomm card offers competitive energy
efficiency, leads in tokens per second per watt on <strong>3 of 12</strong> models, and can serve models on far less hardware
(one 36 W device for an 8B model). It also serves most models at about 6–25 tok/s, which leaves room to grow.</p>

<p>Both sides ran vLLM, so every number measures the chip and its software stack together. I’m re-running the study with
Modular’s <a href="https://max.modular.com/">MAX</a> serving engine alongside vLLM, to measure how much a newer software stack
adds. The four hypotheses, written down before any runs, are on the <a href="/modlearn/">home page</a>.
The code and data are at <a href="https://github.com/kredd2506/modlearn">github.com/kredd2506/modlearn</a>.
For why I chose this paper and what I read in its numbers, see
<a href="/modlearn/2026/09/28/why-this-paper.html">Chip or software?</a>.</p>

<p>This first post is not about Qualcomm or NVIDIA at all. It’s about testing the stopwatch before timing the race.</p>

<h2 id="why-pilot-on-a-laptop">Why pilot on a laptop</h2>

<p>GPU hours cost money, and Qualcomm access is scarce. A benchmark bug found on rented hardware wastes both, and it’s
easy to miss, because a biased measurement still produces a tidy table.</p>

<p>So I built the harness on my MacBook Pro (Apple M4 Pro, 24 GB) first, with MAX 26.6.0 serving
<a href="https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct">SmolLM2-135M-Instruct</a> in bf16 on the Apple GPU.
<strong>None of the numbers below are comparable to the paper.</strong> The model, the hardware, and the power meter are all
different. The point was to check the method, and in two places it needed fixing.</p>

<h2 id="the-method-copied-from-the-paper">The method (copied from the paper)</h2>

<p>The paper doesn’t publish its prompts or code, so I rebuilt its protocol from the text:</p>

<ul>
  <li><strong>Test grid:</strong> output lengths of {10, 50, 100, 200, 1024, 2048} tokens × concurrency {1, 2, 4, 16, 32}.
<em>Concurrency</em> is how many requests are sent at the same moment. That’s 30 cells, and each cell is repeated 3 times.</li>
  <li><strong>Client:</strong> Python <code class="language-plaintext highlighter-rouge">requests</code> plus a <code class="language-plaintext highlighter-rouge">ThreadPoolExecutor</code>, one thread per concurrent request, temperature 0,
against the server’s OpenAI-compatible endpoint. My 20 fixed prompts are in
<a href="https://github.com/kredd2506/modlearn/blob/main/bench/prompts.jsonl"><code class="language-plaintext highlighter-rouge">bench/prompts.jsonl</code></a>.</li>
  <li><strong>Throughput:</strong> total generated tokens divided by the slowest request’s completion time in the batch.</li>
  <li><strong>Power:</strong> sampled once per second and averaged over the run. The paper used <code class="language-plaintext highlighter-rouge">nvidia-smi</code> and <code class="language-plaintext highlighter-rouge">qaic-util</code>. On the
Mac I use <code class="language-plaintext highlighter-rouge">powermetrics</code> (its <code class="language-plaintext highlighter-rouge">gpu_power</code> sampler), which reports GPU power only, not the whole machine.</li>
  <li><strong>Efficiency:</strong> tokens per second per watt (<strong>tok/s/W</strong>), which is the same thing as tokens per joule. Higher is
better.</li>
</ul>

<p>I also stream the responses, which adds two latency numbers the paper doesn’t report: <strong>TTFT</strong> (time to first token,
from sending the request to receiving the first token) and <strong>ITL</strong> (inter-token latency, the average gap between
subsequent tokens). MAX honours <code class="language-plaintext highlighter-rouge">ignore_eos</code>, so every request generates exactly its target number of tokens.</p>

<p>The harness is <a href="https://github.com/kredd2506/modlearn/blob/main/bench/run.py"><code class="language-plaintext highlighter-rouge">bench/run.py</code></a>,
<a href="https://github.com/kredd2506/modlearn/blob/main/bench/client.py"><code class="language-plaintext highlighter-rouge">bench/client.py</code></a>, and
<a href="https://github.com/kredd2506/modlearn/blob/main/bench/power.py"><code class="language-plaintext highlighter-rouge">bench/power.py</code></a>.</p>

<h2 id="aside-getting-max-onto-the-mac-gpu">Aside: getting MAX onto the Mac GPU</h2>

<p>Three things I’d want to know up front:</p>

<ul>
  <li><strong>You need full Xcode, not just the Command Line Tools,</strong> plus <code class="language-plaintext highlighter-rouge">xcodebuild -downloadComponent MetalToolchain</code>.
Without them, serving fails with “Metal Compiler failed to compile metallib”. CPU serving works without either.</li>
  <li><strong>The default KV cache doesn’t fit.</strong> MAX tried to allocate 13.3 GB, which is more than Metal’s maximum buffer size
on a 24 GB machine. <code class="language-plaintext highlighter-rouge">--device-memory-utilization 0.5</code> fixed it.</li>
  <li><strong>Model support is narrower than vLLM’s.</strong> MAX 26.6 has no GPT-2 and no AWQ, and it offers bf16 rather than fp16 for
most architectures. The paper used fp16, so bf16 on every engine is a deviation I’ll carry through the project.</li>
</ul>

<h2 id="first-numbers">First numbers</h2>

<p>The final pilot run covered all 90 cells (30 cells × 3 repeats) with no errors, and every request came back at its
exact target length. Throughput scales well with concurrency:</p>

<p><img src="/modlearn/assets/modlearn/pilot-throughput.png" alt="Throughput vs concurrency for six output lengths. All lines rise roughly linearly on log-log axes; 200-token outputs go from 86 to 1,990 tok/s." /></p>

<p>Here is the 200-token row, which contains the paper’s headline cell (200 tokens × 4). Each value is the median over
3 repeats.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">Concurrency</th>
      <th style="text-align: right">Throughput (tok/s)</th>
      <th style="text-align: right">GPU power (W)</th>
      <th style="text-align: right">tok/s/W</th>
      <th style="text-align: right">TTFT p50 (ms)</th>
      <th style="text-align: right">ITL (ms)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">1</td>
      <td style="text-align: right">86</td>
      <td style="text-align: right">4.6</td>
      <td style="text-align: right">19.5</td>
      <td style="text-align: right">67</td>
      <td style="text-align: right">11.0</td>
    </tr>
    <tr>
      <td style="text-align: right">2</td>
      <td style="text-align: right">150</td>
      <td style="text-align: right">8.1</td>
      <td style="text-align: right">18.7</td>
      <td style="text-align: right">160</td>
      <td style="text-align: right">12.6</td>
    </tr>
    <tr>
      <td style="text-align: right"><strong>4</strong></td>
      <td style="text-align: right"><strong>328</strong></td>
      <td style="text-align: right"><strong>8.9</strong></td>
      <td style="text-align: right"><strong>35.6</strong></td>
      <td style="text-align: right"><strong>71</strong></td>
      <td style="text-align: right"><strong>11.5</strong></td>
    </tr>
    <tr>
      <td style="text-align: right">16</td>
      <td style="text-align: right">1,116</td>
      <td style="text-align: right">10.2</td>
      <td style="text-align: right">108.1</td>
      <td style="text-align: right">297</td>
      <td style="text-align: right">12.8</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td style="text-align: right">1,990</td>
      <td style="text-align: right">12.6</td>
      <td style="text-align: right">158.5</td>
      <td style="text-align: right">404</td>
      <td style="text-align: right">14.2</td>
    </tr>
  </tbody>
</table>

<p>From concurrency 1 to 32, throughput grows about 23× while GPU power grows less than 3×, so efficiency peaks at
about 158 tok/s/W at 200 × 32. That’s the best cell in the grid. With the model loaded and no requests, the GPU
draws essentially nothing (0.07 W), because it power-gates: it switches itself off when idle.</p>

<p>One thing I didn’t expect: <strong>efficiency dips at concurrency 2.</strong> At every output length, tok/s/W at concurrency 2 is
3–30% lower than at 1. Throughput roughly doubles, but GPU power more than doubles, from about 3.5 W to about 8 W at
the short lengths. My guess is that a second request pushes the GPU into a higher clock or power state, but I haven’t
tested that, so treat it as an observation rather than an explanation.</p>

<p>Full grids are in
<a href="https://github.com/kredd2506/modlearn/blob/main/results/pilot-mac-smollm2-135m-minwin5-cells.csv"><code class="language-plaintext highlighter-rouge">results/pilot-mac-smollm2-135m-minwin5-cells.csv</code></a>.</p>

<h2 id="finding-1-short-batches-make-power-look-too-good">Finding 1: short batches make power look too good</h2>

<p>My first full run produced a strange efficiency grid. The best tok/s/W at concurrency 1 came from the <em>shortest</em>
outputs: 201 tok/s/W at 10 tokens, versus 19 at 2048. Nothing about generating fewer tokens should make a GPU ten
times more efficient.</p>

<p>The cause was the measurement window. A 10-token batch at concurrency 1 finishes in about 0.18 s (about 0.35 s
if the GPU first has to wake up; see finding 2), but each
<code class="language-plaintext highlighter-rouge">powermetrics</code> sample covers 1 s. The Apple GPU power-gates to about 0 W between bursts, so each sample averaged a
short burst of work with a long stretch of idle. The reported power came out far too low, and efficiency far too
high.</p>

<p>The fix was simple: repeat each cell’s batch back to back until the measurement window lasts at least 5 s
(<code class="language-plaintext highlighter-rouge">min_window_s = 5</code> in the runner). A 10-token cell now runs 11–30 batches, while a 2048-token cell still runs once.</p>

<p><img src="/modlearn/assets/modlearn/pilot-power-bias.png" alt="Left: at concurrency 1, tok/s/W before and after the fix. The 10-token cell drops from 201 to 16; the 2048-token cell goes from 19 to 17. Right: the before/after ratio for all 30 cells against how long the original batch lasted. Ratios reach 12× for batches under 0.5 s and settle near 1× for batches over 10 s." /></p>

<p>How much a cell moved depends on how long its original batch lasted. Sub-second cells were overstated by 2–12×.
Cells that already lasted 10 s or more should not move, and at concurrency 2–32 the 1024- and 2048-token cells match
the first run within 5%. The two concurrency-1 cells moved more: tok/s/W fell 16% (1024) and 10% (2048). I don’t have a clean
explanation for those. The 2048 × 1 repeats already spread from 16 to 22 tok/s/W within each run, so I read it as
noise rather than bias, but I’ll watch for it on the A100.</p>

<p>The measurements also got steadier. The coefficient of variation of tok/s/W across repeats (standard deviation over
mean) fell from a median of 9.5% (worst 141%) to 5.5% (worst 29%).</p>

<p><strong>Why this matters for the paper:</strong> it also sampled power once per second with <code class="language-plaintext highlighter-rouge">nvidia-smi</code>, and its short cells
(10 and 50 tokens) can finish in well under a second. An A100 doesn’t idle near 0 W the way an Apple GPU does, so the
effect there would be smaller, and I can’t tell from the paper how its windows were set up. So I’m not claiming the
paper’s numbers are wrong. It’s a plausible exposure that I’ll avoid by design, and one I can test directly on an
A100.</p>

<h2 id="finding-2-the-bimodal-ttft-was-the-gpu-waking-up">Finding 2: the bimodal TTFT was the GPU waking up</h2>

<p>The TTFT numbers fell into two groups, around 65 ms and around 250 ms, with little in between. The slow ones weren’t
tied to particular prompts or prompt lengths. They were almost all in <strong>the first batch of each cell</strong>, right after
the runner had paused to collect power samples.</p>

<p>That suggested the GPU was falling asleep between cells. To test it, I sent single requests after controlled idle
gaps of 0 to 5 s, 8 rounds per gap, to MAX on the GPU and to MAX on the CPU as a control
(<a href="https://github.com/kredd2506/modlearn/blob/main/results/ttft-gap-mac.csv"><code class="language-plaintext highlighter-rouge">results/ttft-gap-mac.csv</code></a>):</p>

<p><img src="/modlearn/assets/modlearn/pilot-ttft-gap.png" alt="TTFT vs idle gap. GPU TTFT rises slowly from 66 ms at no gap to 100 ms at 1 s, then jumps to about 260 ms at 2 s and 5 s. CPU TTFT stays between 29 and 51 ms with no step." /></p>

<p>Between 1 s and 2 s of idle, GPU TTFT jumps by about 160 ms, from 100 ms to 262 ms. The CPU shows no step, only a
gentle drift of about 20 ms as the gap grows (the GPU drifts a little more before its step), likely from scheduling
or CPU power states. A cost that appears only on the GPU, and only after a second or two of idle, fits the GPU
waking from power-gating, the same behaviour behind the ~0 W idle reading.</p>

<p>The fix is one unmeasured “prewarm” request just before each cell starts. In a check with 2.5 s of idle between
cells, it brought the median first-batch TTFT from 217 ms down to 67 ms. This is the faithful choice, not a cosmetic
one, because the paper reports steady-state serving, not cold starts. (The runs in this post predate the prewarm, so
cells with only one or two batches, such as 200 × 2 in the table above, still carry some wake-up TTFT.)</p>

<h2 id="finding-3-my-client-agrees-with-modulars-benchmark-tool">Finding 3: my client agrees with Modular’s benchmark tool</h2>

<p>A homemade client needs an outside reference, so I ran MAX’s own <code class="language-plaintext highlighter-rouge">max benchmark</code> against the same server with
200-token outputs
(<a href="https://github.com/kredd2506/modlearn/blob/main/results/crosscheck/max-benchmark-c4-summary.json">c=4</a>,
<a href="https://github.com/kredd2506/modlearn/blob/main/results/crosscheck/max-benchmark-c32-summary.json">c=32</a>):</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">Concurrency</th>
      <th>Metric</th>
      <th style="text-align: right">My client</th>
      <th style="text-align: right"><code class="language-plaintext highlighter-rouge">max benchmark</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">4</td>
      <td>Throughput</td>
      <td style="text-align: right">328 tok/s</td>
      <td style="text-align: right">324 tok/s</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td>TTFT p50 / mean ITL</td>
      <td style="text-align: right">71 / 11.5 ms</td>
      <td style="text-align: right">72 / 11.9 ms</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td>Throughput</td>
      <td style="text-align: right">1,990 tok/s</td>
      <td style="text-align: right">1,968 tok/s</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td>TTFT p50 / mean ITL</td>
      <td style="text-align: right">404 / 14.2 ms</td>
      <td style="text-align: right">325 / 13.9 ms</td>
    </tr>
  </tbody>
</table>

<p>Throughput and ITL agree within 1–3%. TTFT at 32 differs by design. My client releases all 32 requests at once, as
the paper did, so they queue for prefill together. <code class="language-plaintext highlighter-rouge">max benchmark</code> keeps 32 requests in flight on a rolling basis,
so their arrivals are staggered.</p>

<p>Two details if you use <code class="language-plaintext highlighter-rouge">max benchmark</code> yourself. First, its “Output throughput” row is a per-request figure, not the
server’s aggregate throughput, so I computed total output tokens divided by duration. Second, its random dataset
doesn’t force the output length (these runs ranged from 9 to 242 tokens).</p>

<h2 id="what-this-does-and-doesnt-show">What this does and doesn’t show</h2>

<p>It shows that the harness measures what I think it measures: exact output lengths, throughput that matches an
independent tool, power windows long enough to trust, and latency measured warm.</p>

<p>It doesn’t tell you anything about Qualcomm vs NVIDIA:</p>

<ul>
  <li><strong>SmolLM2-135M is a toy.</strong> The paper’s models are much larger, and a 135M model is dominated by per-request and
per-step overheads that matter far less at that scale.</li>
  <li><strong>An Apple GPU is not an A100.</strong> It has a different memory system, different power management, and different
kernels inside MAX.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">powermetrics</code> GPU power is not board power.</strong> <code class="language-plaintext highlighter-rouge">nvidia-smi</code> reports the whole card, including memory. On the Mac,
the unified memory and the CPU running the server aren’t counted.</li>
  <li><strong>bf16, not fp16,</strong> as noted above.</li>
</ul>

<h2 id="whats-next">What’s next</h2>

<ul>
  <li><strong>A100: vLLM vs MAX</strong> on the paper’s grid, using NVML’s energy counter alongside 1 s sampling. That tests H2 and
H3, and whether 1 s sampling biases the paper’s short cells on real data-center hardware.</li>
  <li><strong>Qualcomm Cloud AI 100 Ultra access,</strong> for H1. That’s the question that started this.</li>
  <li><strong>GPT-2 in MAX.</strong> MAX 26.6 doesn’t support GPT-2, so it can’t serve every model on the paper’s list as-is. Writing
that architecture myself is also a good way to learn the MAX graph API.</li>
</ul>

<p>Every setting and every result file from this pilot is in
<a href="https://github.com/kredd2506/modlearn/tree/main/results"><code class="language-plaintext highlighter-rouge">results/</code></a>, with the caveats in its
<a href="https://github.com/kredd2506/modlearn/blob/main/results/README.md">README</a>. The figures come from
<a href="https://github.com/kredd2506/modlearn/blob/main/analysis/plot_pilot.py"><code class="language-plaintext highlighter-rouge">analysis/plot_pilot.py</code></a>.</p>]]></content><author><name></name></author><category term="max" /><category term="modular" /><category term="benchmarking" /><category term="llm-inference" /><category term="power" /><category term="apple-silicon" /><summary type="html"><![CDATA[Before re-running a Qualcomm vs NVIDIA LLM-serving study on rented GPUs, I built and tested the benchmark on a MacBook. The pilot caught a power-sampling bias that inflated efficiency by up to 12× and a GPU wake-up delay hiding in the latency numbers, and my client matched Modular's own benchmark tool within 1–3%.]]></summary></entry><entry><title type="html">Chip or software? Why I’m re-running the Qualcomm Cloud AI 100 vs A100 study on Modular MAX</title><link href="https://kredd2506.github.io/modlearn/2026/09/28/why-this-paper.html" rel="alternate" type="text/html" title="Chip or software? Why I’m re-running the Qualcomm Cloud AI 100 vs A100 study on Modular MAX" /><published>2026-09-28T00:00:00+00:00</published><updated>2026-09-28T00:00:00+00:00</updated><id>https://kredd2506.github.io/modlearn/2026/09/28/why-this-paper</id><content type="html" xml:base="https://kredd2506.github.io/modlearn/2026/09/28/why-this-paper.html"><![CDATA[<p>In July 2025, a team at UC San Diego published
<a href="https://arxiv.org/abs/2507.00418"><em>Serving LLMs in HPC Clusters: A Comparative Study of Qualcomm Cloud AI 100 Ultra and NVIDIA Data Center GPUs</em></a>
(Sada et al.; I’m reading v3, October 2025). A year later, Qualcomm
<a href="https://www.prnewswire.com/news-releases/qualcomm-completes-acquisition-of-modular-302837286.html">completed its acquisition of Modular</a>,
the company behind the MAX inference framework and the Mojo language. A month after that, Modular showed
<a href="https://www.modular.com/blog/modcon-qualcomm">MAX running on the same Qualcomm accelerator</a>.</p>

<p>Those three events raise a question the paper had no way to ask: <strong>how much better does this chip get with a newer
software stack?</strong> This post explains what the paper found, why its numbers suggest there’s more to get from the
hardware, and how I plan to measure it. The <a href="/modlearn/2026/09/28/benchmarking-before-benchmarking.html">first results post</a>
covers how I built and validated the benchmark harness.</p>

<h2 id="why-inference-and-why-now">Why inference, and why now</h2>

<p>Training a model is a one-time cost. Serving it is paid on every request, for as long as the model is in use, and
for most organizations running LLMs that’s now where most of the compute and the power bill goes. At that point
the numbers that matter are <strong>tokens per joule</strong> and <strong>how finely you can divide hardware among users</strong>, not peak
FLOPS. Accelerators built for inference, like Qualcomm’s Cloud AI 100, are designed around exactly those two
numbers. The question is whether the software lets them deliver.</p>

<h2 id="the-paper-in-one-paragraph">The paper in one paragraph</h2>

<p>The authors serve 12 open models, from GPT-2 (124M parameters) to three 70B models, on two kinds of hardware in the
<a href="https://nationalresearchplatform.org/">National Research Platform</a> (NRP), a shared Kubernetes cluster used by more than
300 research groups. One side is a node with 8 <strong>Qualcomm Cloud AI 100 Ultra</strong> cards at the San Diego Supercomputer
Center. Each card contains four independent chips (“devices”). The other side is nodes with 4 or 8 <strong>NVIDIA A100 80 GB</strong> GPUs.
Both sides serve through <strong>vLLM</strong>: stock vLLM on the A100s, and a Qualcomm-provided vLLM fork on Cloud AI 100 (SDK 1.20),
where each model is first exported to ONNX and compiled ahead of time into a Qualcomm Program Container (QPC). The
benchmark sweeps output length (10 to 2,048 tokens) × concurrent requests (1 to 32), and it measures throughput and
accelerator power.</p>

<h2 id="what-the-paper-found">What the paper found</h2>

<p>In the authors’ words, Qualcomm “achieves competitive energy efficiency with advantages on specific models while
enabling more granular hardware allocation.” Their examples make the granularity point concrete:</p>

<ul>
  <li><strong>Small models on a quarter of a card.</strong> granite-3.2-8b serves from a single Cloud AI 100 device at <strong>36 W</strong>, where
the A100 setup used four GPUs at 1,246 W.</li>
  <li><strong>A 70B model on one card.</strong> Nemotron-70B runs on one card (four devices) at <strong>148 W</strong>, where the A100 setup used
eight GPUs at 2,983 W.</li>
</ul>

<p>For a shared cluster serving hundreds of groups, being able to hand out a 36 W slice instead of a whole 80 GB GPU is
an operational advantage in its own right.</p>

<p>The paper is also clear that A100s deliver higher absolute throughput, and it frames Qualcomm’s case around power
budgets and granularity. Plotting both halves of its Table 2 (200 output tokens, 4 parallel requests) shows where
Qualcomm already leads:</p>

<p><img src="/modlearn/assets/modlearn/paper-power-vs-throughput.png" alt="For each of 12 models, how many times less power Qualcomm draws (7 to 35×) and how many times less throughput it delivers (12 to 75×). For granite-3.2-8b, GPT-2 and Nemotron-70B, the power reduction is larger than the throughput reduction." /></p>

<p>Where the power saving (blue) is larger than the throughput gap (orange), Qualcomm produces more tokens per joule.
Dividing one by the other gives the efficiency ratio, which the paper reports in its Table 3:</p>

<p><img src="/modlearn/assets/modlearn/paper-efficiency-ratio.png" alt="Qualcomm tokens per second per watt divided by A100's, per model, on a log scale. Above parity: granite-3.2-8b 2.72×, GPT-2 2.65×, Nemotron-70B 1.16×. Below parity: nine models from 0.77× down to 0.13×." /></p>

<p>These are the “specific models” in the abstract: <strong>granite-3.2-8b (2.7×), GPT-2 (2.7×), and Nemotron-70B (1.16×)</strong>,
on 2025 software. This chart is the baseline I want to move. If a new stack raises Qualcomm’s throughput without
raising its power much, bars move right, and the count of models above parity goes up.</p>

<h2 id="the-headroom-is-in-the-software">The headroom is in the software</h2>

<p>The most interesting numbers in the paper are the ones that don’t line up, because they point at what software could
fix. Two pairs of models stand out:</p>

<table>
  <thead>
    <tr>
      <th>Pair</th>
      <th>Qualcomm devices</th>
      <th>Throughput</th>
      <th>Power</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>granite-3.2-8b</td>
      <td>1</td>
      <td>25 tok/s</td>
      <td>36 W</td>
    </tr>
    <tr>
      <td>DeepSeek-R1-Distill-Llama-8B (same size)</td>
      <td>4</td>
      <td>24 tok/s</td>
      <td>140 W</td>
    </tr>
    <tr>
      <td>DeepSeek-R1-Distill-Llama-70B</td>
      <td>8</td>
      <td>8 tok/s</td>
      <td>292 W</td>
    </tr>
    <tr>
      <td>Nemotron-70B (same Llama-70B architecture)</td>
      <td>4</td>
      <td>6 tok/s</td>
      <td>148 W</td>
    </tr>
  </tbody>
</table>

<p>In both pairs, similar models get similar throughput from very different amounts of silicon. If the chip’s raw
capability set the limit, four devices would do more than one. Something between the model and the silicon is doing
the limiting: how the model is exported, compiled, split across devices, and scheduled.</p>

<p>Two more signs point the same way:</p>

<ul>
  <li><strong>Batching is fixed at compile time.</strong> Each Qualcomm model is compiled ahead of time for a batch size of 1, 2, or 4,
and the paper notes that “compilation can take hours.” To compare like with like, the authors capped vLLM on the
A100 to the same batch size. So neither side batched more than four requests, even in the 16- and 32-request tests.
Batching many requests together is the main way servers turn spare compute into throughput.</li>
  <li><strong>Per-user speed is low.</strong> Most models serve at 6–25 tok/s across four concurrent requests, a few tokens per second
for each user. GPT-2, which is small enough to be compute-light, reaches 218 tok/s on a single device. The hardware
can clearly move faster than most of these numbers show.</li>
</ul>

<p>The paper doesn’t say which precision each Qualcomm model was compiled at (FP16 or INT8) or which batch size, and
those may explain part of the gaps above. But precision and batch size are software choices too. Every step in the
path is software: the ONNX export, the ahead-of-time compile, the device partitioning, and a vLLM fork maintained
separately from upstream vLLM.</p>

<p>I can’t tell from outside how much of the gap software can close. That’s what the re-run is for.</p>

<h2 id="why-max-is-the-stack-to-test">Why MAX is the stack to test</h2>

<p>On July 29, 2026, Qualcomm completed its acquisition of Modular and said that “Mojo, MAX, and Modular Cloud will
continue as products and brands.” Modular’s pitch has always been portability without a performance penalty: one
serving stack (MAX), one kernel language (Mojo), many kinds of hardware. At ModCon in August, the Modular team wrote
that “the same code that runs on NVIDIA and AMD GPUs can now target Qualcomm Technologies’ silicon.” They showed GPT-2
reaching “6.7× the baseline” after about three weeks of optimization, and Gemma 4 31B served across a card’s four
devices with tensor parallelism.</p>

<p>That 6.7× is measured against a <em>PyTorch implementation</em> on Cloud AI 100, not against the paper’s vLLM setup, so the two
can’t be put side by side yet. This paper is the natural yardstick to close that gap:</p>

<ul>
  <li><strong>It’s the best public “before” picture.</strong> It’s an independent, published measurement of Qualcomm’s inference stack
before MAX, with power, on the accelerator Modular now targets. If MAX unlocks the hardware, it shows up here as more
throughput at similar watts.</li>
  <li><strong>Its protocol is concrete enough to reproduce.</strong> The paper spells out the test grid, the client (Python <code class="language-plaintext highlighter-rouge">requests</code>
plus a thread pool), how throughput is computed, and how power is sampled. That makes a re-run with a different
software stack a like-for-like comparison, not a new benchmark.</li>
  <li><strong>The batch limit has a measurable answer.</strong> If MAX can serve Cloud AI 100 at 16 or 32 concurrent requests without a
fixed-batch compile, that changes what the chip is good for.</li>
  <li><strong>Portability has to hold on both sides.</strong> Running MAX on the A100 against vLLM checks that a portable stack doesn’t
cost performance on NVIDIA hardware. Running MAX on Qualcomm checks that it helps the new hardware.</li>
  <li><strong>“Same code” is testable.</strong> Writing one Mojo kernel and running it unchanged on an Apple GPU, an NVIDIA GPU, and,
with luck, a Qualcomm device is the most direct check of the portability claim I can do from outside.</li>
</ul>

<h2 id="the-experiment">The experiment</h2>

<p>I keep everything the paper specifies, run it on both engines, and set up both sides as well as each can be set up:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Paper</th>
      <th>This re-run</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Test grid, client, throughput formula</td>
      <td>6 lengths × 5 concurrency, requests + thread pool</td>
      <td><strong>Same</strong></td>
    </tr>
    <tr>
      <td>Engines</td>
      <td>vLLM on both sides</td>
      <td><strong>vLLM and MAX</strong> on A100; <strong>MAX</strong> on Qualcomm</td>
    </tr>
    <tr>
      <td>Batch cap</td>
      <td>Both capped at the QPC batch (1, 2, or 4)</td>
      <td><strong>Both</strong>: a paper-faithful capped run <em>and</em> each system at its best</td>
    </tr>
    <tr>
      <td>Hardware per model</td>
      <td>Qualcomm 1–8 devices; A100 4 or 8 GPUs</td>
      <td>The fewest that fit on each side (1 A100 for ≤8B models), plus the paper’s setup for a few models so the numbers line up</td>
    </tr>
    <tr>
      <td>Precision</td>
      <td>A100 fp16; Qualcomm fp16 or INT8</td>
      <td><strong>bf16 on every engine</strong>, stated per row (MAX offers bf16, not fp16, for most models)</td>
    </tr>
    <tr>
      <td>Power</td>
      <td>1 s sampling, accelerator only</td>
      <td>1 s sampling <strong>plus</strong> NVML’s energy counter, windows of <strong>≥5 s</strong>, <strong>idle measured</strong></td>
    </tr>
    <tr>
      <td>Latency</td>
      <td>End-to-end</td>
      <td>Also time to first token and inter-token latency, measured warm</td>
    </tr>
    <tr>
      <td>Reporting</td>
      <td>The 200-token × 4-request cell</td>
      <td><strong>All 30 cells</strong>, prompts, code, and raw data <a href="https://github.com/kredd2506/modlearn">on GitHub</a></td>
    </tr>
  </tbody>
</table>

<p>Right-sizing works in both directions. Fewer A100s for small models will likely improve the A100’s tokens per watt,
and lifting the batch cap should help both chips. I’ll report both effects.</p>

<h2 id="the-hypotheses-stated-before-the-runs">The hypotheses, stated before the runs</h2>

<p>Each one comes with what would count as failure.</p>

<ul>
  <li><strong>H1, Qualcomm:</strong> MAX serves the paper’s models on Cloud AI 100 Ultra at substantially higher throughput than the
paper’s numbers, at similar power, so more models land above parity. <em>This fails if the throughput stays in the
same range.</em></li>
  <li><strong>H2, A100:</strong> MAX matches or beats vLLM in throughput and tok/s/W on the same GPU. <em>This fails if MAX is slower
or less efficient in most cells.</em></li>
  <li><strong>H3, right-sized A100:</strong> serving ≤8B models on 1 A100 instead of 4 raises the A100’s tokens per watt, so the fair
comparison on those models is closer than Table 2 shows. <em>This fails if the A100’s efficiency doesn’t improve.</em></li>
  <li><strong>H4, Mojo:</strong> one Mojo kernel runs unchanged on Apple, NVIDIA, and Qualcomm silicon. <em>This fails if it needs
per-chip rewrites.</em></li>
</ul>

<p>I’ll report every result with the same prominence, whichever way it goes. An independent number is only useful if it
could have come out the other way, and that matters more now that Qualcomm owns both the chip and the software stack.
The biggest risk is H1: I don’t yet have access to Qualcomm hardware. If that doesn’t change, H1 will be reported as
unanswered, and H2–H4 will still stand on their own.</p>

<h2 id="whats-next">What’s next</h2>

<p>The harness is built and validated. The
<a href="/modlearn/2026/09/28/benchmarking-before-benchmarking.html">pilot post</a> covers the three measurement
problems it caught on a MacBook before any GPU money was spent. Next come A100 runs, vLLM against MAX, and the search
for Qualcomm access.</p>

<p>The figures in this post come from the paper’s Table 2, transcribed to
<a href="https://github.com/kredd2506/modlearn/blob/main/results/paper/sada2025-table2.csv"><code class="language-plaintext highlighter-rouge">results/paper/sada2025-table2.csv</code></a>
and plotted by <a href="https://github.com/kredd2506/modlearn/blob/main/analysis/plot_paper.py"><code class="language-plaintext highlighter-rouge">analysis/plot_paper.py</code></a>.</p>

<p><em>Updated September 29, 2026: reframed around what a new software stack could add, and corrected how I described the
paper’s findings. An earlier version attributed the “10–35×” power figure to the abstract; it comes from the
conclusion, and the abstract describes efficiency as competitive, with advantages on specific models.</em></p>]]></content><author><name></name></author><category term="qualcomm" /><category term="nvidia" /><category term="max" /><category term="mojo" /><category term="modular" /><category term="llm-inference" /><category term="energy-efficiency" /><summary type="html"><![CDATA[A 2025 UCSD study served 12 LLMs on Qualcomm's Cloud AI 100 Ultra and on NVIDIA A100s, both through vLLM. It found competitive energy efficiency, a clear lead on some models, and far finer hardware allocation. Its own tables also show headroom that looks like software. Qualcomm now owns Modular, so I'm re-running the study on MAX to measure how much of that headroom a new stack can claim.]]></summary></entry></feed>