<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Runcrate</title>
    <description>The latest articles on DEV Community by Runcrate (@runcrate).</description>
    <link>https://dev.to/runcrate</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4196817%2Fac60595c-4ddb-4983-9e0e-2ba9145a50b5.png</url>
      <title>DEV Community: Runcrate</title>
      <link>https://dev.to/runcrate</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/runcrate"/>
    <language>en</language>
    <item>
      <title>torch.compile vs eager for training a 468M Llama-style model on four GPUs: step time, memory and compile cost</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Thu, 29 Oct 2026 03:24:55 +0000</pubDate>
      <link>https://dev.to/runcrate/torchcompile-vs-eager-for-training-a-468m-llama-style-model-on-four-gpus-step-time-memory-and-2ebo</link>
      <guid>https://dev.to/runcrate/torchcompile-vs-eager-for-training-a-468m-llama-style-model-on-four-gpus-step-time-memory-and-2ebo</guid>
      <description>&lt;p&gt;Does &lt;code&gt;torch.compile&lt;/code&gt; make a small transformer train faster? I rented four GPUs on Runcrate today (2026-10-29), an RTX 5090, an L40S, an RTX A6000 and an H100 PCIe, trained the same 468M-parameter Llama-style model in eager mode and compiled, with PyTorch's SDPA attention and with the plain "eager" attention, and deleted every machine when the numbers were saved.&lt;/p&gt;

&lt;p&gt;Short version: with SDPA attention, &lt;code&gt;torch.compile&lt;/code&gt; changed the step time by between 9% slower (RTX 5090) and 3% faster (L40S), cost 22 to 39 s of compile time on the first step, and cut peak memory by 23% on every GPU (17.3 to 13.3 GB). With the plain attention it was a different story: on the H100 it made the step 2.6x faster and fit the model in 39 GB instead of 56 GB, and on the 48 GB cards it was the only way the run fit at all. Even then, eager mode with SDPA beat compiled plain attention on every GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPUs (one per machine), on-demand price at launch time:&lt;/strong&gt; RTX 5090 32 GB (Austin, US, $1.15 per hour), L40S 48 GB (Des Moines, US, $0.54), RTX A6000 48 GB (Montreal, CA, $0.65), H100 PCIe 80 GB (US Midwest, $3.86).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;code&gt;LlamaForCausalLM&lt;/code&gt; from a config, random init: hidden 1024, 24 layers, 16 heads, FFN 4096, vocab 32,000, 468.2M parameters, weights in bf16.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training step:&lt;/strong&gt; batch 4 x 2,048 tokens (8,192 tokens per step) of random token ids, &lt;code&gt;loss.backward()&lt;/code&gt;, fused AdamW (lr 1e-4), &lt;code&gt;zero_grad(set_to_none=True)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four setups, each in its own process:&lt;/strong&gt; eager or &lt;code&gt;torch.compile(model)&lt;/code&gt; (default mode), times &lt;code&gt;attn_implementation="sdpa"&lt;/code&gt; or &lt;code&gt;"eager"&lt;/code&gt; (the naive matmul-softmax-matmul attention in transformers).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measured:&lt;/strong&gt; first step time (includes compilation), then 4 more warm-up steps, then the mean of 20 timed steps, tokens per second, and peak memory from &lt;code&gt;torch.cuda.max_memory_allocated&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software:&lt;/strong&gt; torch 2.8.0+cu128, transformers 4.57.1, Python 3.12, the same on all four machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LlamaConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LlamaForCausalLM&lt;/span&gt;
&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# eager|compile, sdpa|eager
&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LlamaConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vocab_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hidden_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intermediate_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_hidden_layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;num_attention_heads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_key_value_heads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_position_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;attn_implementation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;attn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LlamaForCausalLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;opt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AdamW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fused&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fwd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;loss&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set_to_none&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset_peak_memory_stats&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_step_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;step_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tokens_per_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;peak_gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max_memory_allocated&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Step (ms)&lt;/th&gt;
&lt;th&gt;Tokens/s&lt;/th&gt;
&lt;th&gt;Peak memory&lt;/th&gt;
&lt;th&gt;First step (s)&lt;/th&gt;
&lt;th&gt;M tokens per $&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;eager + sdpa attention&lt;/td&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;td&gt;41,870&lt;/td&gt;
&lt;td&gt;17.3 GB&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;131&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;compile + sdpa attention&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;38,040&lt;/td&gt;
&lt;td&gt;13.3 GB&lt;/td&gt;
&lt;td&gt;21.6&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;eager + eager attention&lt;/td&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;compile + eager attention&lt;/td&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;eager + sdpa attention&lt;/td&gt;
&lt;td&gt;244&lt;/td&gt;
&lt;td&gt;33,581&lt;/td&gt;
&lt;td&gt;17.3 GB&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;224&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;compile + sdpa attention&lt;/td&gt;
&lt;td&gt;236&lt;/td&gt;
&lt;td&gt;34,691&lt;/td&gt;
&lt;td&gt;13.3 GB&lt;/td&gt;
&lt;td&gt;34.2&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;eager + eager attention&lt;/td&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;compile + eager attention&lt;/td&gt;
&lt;td&gt;391&lt;/td&gt;
&lt;td&gt;20,973&lt;/td&gt;
&lt;td&gt;39.1 GB&lt;/td&gt;
&lt;td&gt;38.9&lt;/td&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;eager + sdpa attention&lt;/td&gt;
&lt;td&gt;457&lt;/td&gt;
&lt;td&gt;17,942&lt;/td&gt;
&lt;td&gt;17.3 GB&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;compile + sdpa attention&lt;/td&gt;
&lt;td&gt;455&lt;/td&gt;
&lt;td&gt;18,005&lt;/td&gt;
&lt;td&gt;13.3 GB&lt;/td&gt;
&lt;td&gt;39.2&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;eager + eager attention&lt;/td&gt;
&lt;td&gt;OOM&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;compile + eager attention&lt;/td&gt;
&lt;td&gt;546&lt;/td&gt;
&lt;td&gt;15,017&lt;/td&gt;
&lt;td&gt;39.1 GB&lt;/td&gt;
&lt;td&gt;47.1&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;eager + sdpa attention&lt;/td&gt;
&lt;td&gt;152&lt;/td&gt;
&lt;td&gt;54,022&lt;/td&gt;
&lt;td&gt;17.4 GB&lt;/td&gt;
&lt;td&gt;0.4&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;compile + sdpa attention&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;td&gt;51,055&lt;/td&gt;
&lt;td&gt;13.4 GB&lt;/td&gt;
&lt;td&gt;36.4&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;eager + eager attention&lt;/td&gt;
&lt;td&gt;435&lt;/td&gt;
&lt;td&gt;18,813&lt;/td&gt;
&lt;td&gt;56.0 GB&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;compile + eager attention&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;td&gt;48,469&lt;/td&gt;
&lt;td&gt;39.1 GB&lt;/td&gt;
&lt;td&gt;43.9&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"M tokens per $" is training tokens per dollar of GPU time, in millions, at each machine's hourly price. OOM means the run did not fit in the GPU's memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;With SDPA attention, compile did not speed up training.&lt;/strong&gt; Step time compiled against eager: RTX 5090 215 vs 196 ms (9% slower), H100 160 vs 152 ms (5% slower), RTX A6000 455 vs 457 ms (same), L40S 236 vs 244 ms (3% faster). SDPA already runs a fused attention kernel, and at this model size the matmuls dominate, so there was little left for the compiler to fuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile did save memory: 4 GB, 23%, on every GPU.&lt;/strong&gt; Peak allocation fell from 17.3 to 13.3 GB. Fused elementwise ops keep fewer intermediate activations for the backward pass, so compile can buy a larger batch even when it does not buy speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compilation took 22 to 47 s.&lt;/strong&gt; (22 to 39 s with SDPA.) For a short experiment that is a real share of the run, and on the RTX 5090 and the H100, where compiled steps were also slower, it never paid back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Naive attention is where compile pays.&lt;/strong&gt; On the H100, eager attention ran at 435 ms per step and 56 GB; compiled, 169 ms (2.6x faster) and 39 GB. On the L40S and the RTX A6000 eager attention ran out of memory without compile and fit with it. On the 32 GB RTX 5090 it did not fit either way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But the right attention kernel beat the compiler.&lt;/strong&gt; Eager mode with SDPA was faster than compiled naive attention on every GPU (H100 152 vs 169 ms, L40S 244 vs 391 ms) and used less than half the memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per dollar, the L40S trained the most tokens:&lt;/strong&gt; 231M tokens per dollar compiled with SDPA, against 131M on the RTX 5090, 100M on the RTX A6000 and 50M on the H100. The H100 was the fastest card (54,022 tokens per second), 1.3x the RTX 5090, at 3.4 times its price.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use SDPA (or FlashAttention) first. That is the large win and costs nothing.&lt;/li&gt;
&lt;li&gt;Then try &lt;code&gt;torch.compile&lt;/code&gt; for memory rather than speed at this size: 23% less activation memory can mean a larger batch or a longer context on the same card.&lt;/li&gt;
&lt;li&gt;Measure on your GPU. The same compiled model was 9% slower on one card and 3% faster on another.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One model size (468M), one batch shape, one run per setup, on one day. Larger models, longer sequences or &lt;code&gt;mode="max-autotune"&lt;/code&gt; can change the result; I tested the default mode only.&lt;/li&gt;
&lt;li&gt;Pure bf16 weights and optimizer state, no mixed precision with fp32 master weights.&lt;/li&gt;
&lt;li&gt;The machines differ in CPU and host; with this batch size the step is GPU-bound, but small differences may come from the host.&lt;/li&gt;
&lt;li&gt;Prices are on-demand at launch time and change daily. Billed cost for the four machines, which also ran an embedding benchmark, was $0.14 (RTX 5090), $0.11 (L40S, which also ran a model startup test), $0.08 (RTX A6000) and $0.43 (H100).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Launch a machine with &lt;code&gt;POST /api/v1/instances&lt;/code&gt; (&lt;code&gt;{"name": "...", "gpu_type": "L40S", "gpu_count": 1, "ssh_key_id": "...", "max_runtime_minutes": 40}&lt;/code&gt;), install torch and &lt;code&gt;transformers==4.57.1&lt;/code&gt; with uv.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python train.py compile sdpa&lt;/code&gt; and the other three combinations, then delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=torch-compile-vs-eager-training" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pytorch</category>
      <category>machinelearning</category>
      <category>gpu</category>
      <category>python</category>
    </item>
    <item>
      <title>Text embedding throughput per dollar: BGE small, base and large on RTX 5090, L40S, RTX A6000 and H100</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Thu, 29 Oct 2026 03:24:15 +0000</pubDate>
      <link>https://dev.to/runcrate/text-embedding-throughput-per-dollar-bge-small-base-and-large-on-rtx-5090-l40s-rtx-a6000-and-nlg</link>
      <guid>https://dev.to/runcrate/text-embedding-throughput-per-dollar-bge-small-base-and-large-on-rtx-5090-l40s-rtx-a6000-and-nlg</guid>
      <description>&lt;p&gt;What does it cost to embed a million passages with an open model, and does a faster GPU earn its price? I rented four GPUs on Runcrate today (2026-10-29), an RTX 5090, an L40S, an RTX A6000 and an H100 PCIe, measured end-to-end &lt;code&gt;encode()&lt;/code&gt; throughput for BGE small, base and large at 128 and 512 tokens, and deleted every machine when the numbers were saved.&lt;/p&gt;

&lt;p&gt;Short version: the L40S, at $0.54 an hour, embedded a million passages for the least money in all six cases: $0.032 per million 128-token passages with bge-small, $0.49 per million 512-token passages with bge-large. The H100 was the fastest card only for bge-base and bge-large, and cost 3.6 to 6.8 times more per passage than the L40S. With bge-small, the H100 was slower than the RTX 5090.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPUs (one per machine), on-demand price at launch time:&lt;/strong&gt; RTX 5090 32 GB (Austin, US, $1.15 per hour, 16 vCPUs), L40S 48 GB (Des Moines, US, $0.54, 12 vCPUs), RTX A6000 48 GB (Montreal, CA, $0.65, 28 vCPUs), H100 PCIe 80 GB (US Midwest, $3.86, 20 vCPUs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models:&lt;/strong&gt; &lt;code&gt;BAAI/bge-small-en-v1.5&lt;/code&gt; (33M parameters), &lt;code&gt;BAAI/bge-base-en-v1.5&lt;/code&gt; (109M), &lt;code&gt;BAAI/bge-large-en-v1.5&lt;/code&gt; (335M), fp16.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software:&lt;/strong&gt; sentence-transformers 5.7.0, transformers 4.57.1, torch 2.8.0+cu128, Python 3.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input:&lt;/strong&gt; 4,096 passages per run made of random words from the model's vocabulary, long enough that every passage fills the window, then truncated to exactly 128 or 512 tokens. The content does not change the compute for an encoder; the fixed length makes runs comparable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measured:&lt;/strong&gt; passages per second over at least 20 s of repeated &lt;code&gt;model.encode(texts, batch_size=128)&lt;/code&gt; after a warm-up, tokenization included, plus peak GPU memory from &lt;code&gt;torch.cuda.max_memory_allocated&lt;/code&gt;. Cost per million passages = hourly price / (passages per second x 3600) x 1,000,000.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;
&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seqlen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;torch_dtype&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float16&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_seq_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seqlen&lt;/span&gt;
&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_vocab&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isalpha&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seqlen&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# warm-up
&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seqlen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;seqlen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passages_per_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;128-token passages, fp16, batch 128&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;Passages/s&lt;/th&gt;
&lt;th&gt;Tokens/s&lt;/th&gt;
&lt;th&gt;Peak memory&lt;/th&gt;
&lt;th&gt;$ per 1M passages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bge-small-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;6,415&lt;/td&gt;
&lt;td&gt;821,094&lt;/td&gt;
&lt;td&gt;0.2 GB&lt;/td&gt;
&lt;td&gt;$0.050&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-small-en-v1.5&lt;/td&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;4,714&lt;/td&gt;
&lt;td&gt;603,443&lt;/td&gt;
&lt;td&gt;0.2 GB&lt;/td&gt;
&lt;td&gt;$0.032&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-small-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;3,681&lt;/td&gt;
&lt;td&gt;471,130&lt;/td&gt;
&lt;td&gt;0.2 GB&lt;/td&gt;
&lt;td&gt;$0.049&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-small-en-v1.5&lt;/td&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;4,952&lt;/td&gt;
&lt;td&gt;633,882&lt;/td&gt;
&lt;td&gt;0.2 GB&lt;/td&gt;
&lt;td&gt;$0.217&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-base-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;4,077&lt;/td&gt;
&lt;td&gt;521,843&lt;/td&gt;
&lt;td&gt;0.5 GB&lt;/td&gt;
&lt;td&gt;$0.078&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-base-en-v1.5&lt;/td&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;3,054&lt;/td&gt;
&lt;td&gt;390,925&lt;/td&gt;
&lt;td&gt;0.5 GB&lt;/td&gt;
&lt;td&gt;$0.049&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-base-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;2,273&lt;/td&gt;
&lt;td&gt;290,931&lt;/td&gt;
&lt;td&gt;0.5 GB&lt;/td&gt;
&lt;td&gt;$0.079&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-base-en-v1.5&lt;/td&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;4,123&lt;/td&gt;
&lt;td&gt;527,706&lt;/td&gt;
&lt;td&gt;0.6 GB&lt;/td&gt;
&lt;td&gt;$0.260&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-large-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;1,748&lt;/td&gt;
&lt;td&gt;223,731&lt;/td&gt;
&lt;td&gt;1.1 GB&lt;/td&gt;
&lt;td&gt;$0.183&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-large-en-v1.5&lt;/td&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;1,374&lt;/td&gt;
&lt;td&gt;175,936&lt;/td&gt;
&lt;td&gt;1.1 GB&lt;/td&gt;
&lt;td&gt;$0.109&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-large-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;875&lt;/td&gt;
&lt;td&gt;111,987&lt;/td&gt;
&lt;td&gt;1.1 GB&lt;/td&gt;
&lt;td&gt;$0.206&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-large-en-v1.5&lt;/td&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;2,391&lt;/td&gt;
&lt;td&gt;306,061&lt;/td&gt;
&lt;td&gt;1.1 GB&lt;/td&gt;
&lt;td&gt;$0.448&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;512-token passages, fp16, batch 128&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;Passages/s&lt;/th&gt;
&lt;th&gt;Tokens/s&lt;/th&gt;
&lt;th&gt;Peak memory&lt;/th&gt;
&lt;th&gt;$ per 1M passages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bge-small-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;1,713&lt;/td&gt;
&lt;td&gt;877,005&lt;/td&gt;
&lt;td&gt;0.7 GB&lt;/td&gt;
&lt;td&gt;$0.186&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-small-en-v1.5&lt;/td&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;1,193&lt;/td&gt;
&lt;td&gt;610,867&lt;/td&gt;
&lt;td&gt;0.7 GB&lt;/td&gt;
&lt;td&gt;$0.126&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-small-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;1,068&lt;/td&gt;
&lt;td&gt;546,867&lt;/td&gt;
&lt;td&gt;0.7 GB&lt;/td&gt;
&lt;td&gt;$0.169&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-small-en-v1.5&lt;/td&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;1,428&lt;/td&gt;
&lt;td&gt;731,085&lt;/td&gt;
&lt;td&gt;0.7 GB&lt;/td&gt;
&lt;td&gt;$0.751&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-base-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;1,033&lt;/td&gt;
&lt;td&gt;528,998&lt;/td&gt;
&lt;td&gt;1.4 GB&lt;/td&gt;
&lt;td&gt;$0.309&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-base-en-v1.5&lt;/td&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;763&lt;/td&gt;
&lt;td&gt;390,605&lt;/td&gt;
&lt;td&gt;1.4 GB&lt;/td&gt;
&lt;td&gt;$0.197&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-base-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;603&lt;/td&gt;
&lt;td&gt;308,838&lt;/td&gt;
&lt;td&gt;1.4 GB&lt;/td&gt;
&lt;td&gt;$0.299&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-base-en-v1.5&lt;/td&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;1,156&lt;/td&gt;
&lt;td&gt;591,974&lt;/td&gt;
&lt;td&gt;1.5 GB&lt;/td&gt;
&lt;td&gt;$0.927&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-large-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.15&lt;/td&gt;
&lt;td&gt;411&lt;/td&gt;
&lt;td&gt;210,330&lt;/td&gt;
&lt;td&gt;2.3 GB&lt;/td&gt;
&lt;td&gt;$0.778&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-large-en-v1.5&lt;/td&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;305&lt;/td&gt;
&lt;td&gt;156,109&lt;/td&gt;
&lt;td&gt;2.3 GB&lt;/td&gt;
&lt;td&gt;$0.492&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-large-en-v1.5&lt;/td&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;234&lt;/td&gt;
&lt;td&gt;120,064&lt;/td&gt;
&lt;td&gt;2.3 GB&lt;/td&gt;
&lt;td&gt;$0.770&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-large-en-v1.5&lt;/td&gt;
&lt;td&gt;H100&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;598&lt;/td&gt;
&lt;td&gt;306,330&lt;/td&gt;
&lt;td&gt;2.3 GB&lt;/td&gt;
&lt;td&gt;$1.792&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The L40S gave the most passages per dollar for every model and length.&lt;/strong&gt; For 10 million 512-token chunks with bge-base that is about $2.0 on the L40S, $3.0 on the RTX A6000, $3.1 on the RTX 5090 and $9.3 on the H100 PCIe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small models don't fill a big GPU.&lt;/strong&gt; bge-small ran at 4,952 passages per second on the H100 and 6,415 on the RTX 5090, and on every card its tokens per second changed by 16% or less between 128 and 512 tokens. The GPU is waiting on tokenization and Python between batches, so the per-batch CPU work and the CPU itself decide the speed, not the GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The H100 pulled ahead only when the model was large enough.&lt;/strong&gt; bge-large at 512 tokens: 598 passages per second on the H100, 411 on the RTX 5090, 305 on the L40S, 234 on the RTX A6000. Still, the L40S did the same work for 27% of the H100's cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is not the constraint.&lt;/strong&gt; Peak allocation was 2.3 GB or less at batch 128, even for bge-large at 512 tokens. Any of these cards could serve several embedding models at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The RTX 5090 and the RTX A6000 cost about the same per passage&lt;/strong&gt; (within 11% on every row), the 5090 doing it 1.6 to 2.0x faster.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For bulk embedding with BGE-class models, a mid-range data-center card at a low hourly price was the efficient choice here. Paying for an H100 bought speed only for the large model, at 3.6 to 4.1 times the L40S's cost per passage.&lt;/li&gt;
&lt;li&gt;If you use a small embedding model, throughput depends on the CPU side: tokenize ahead of time, use a fast tokenizer, or run several workers per GPU before you reach for a bigger card.&lt;/li&gt;
&lt;li&gt;Real corpora have mixed lengths, and sentence-transformers sorts each call's inputs by length, so short real-world text will run faster than these fixed-length 512-token numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One run per setup and machine, on one day. Treat differences under about 10% as noise.&lt;/li&gt;
&lt;li&gt;The machines differ in CPU count (12 to 28 vCPUs) and CPU model, which matters most for the small models (point 2).&lt;/li&gt;
&lt;li&gt;No ONNX, TensorRT or text-embeddings-inference server: plain sentence-transformers in fp16, the way most pipelines start. A dedicated server would raise the small-model numbers.&lt;/li&gt;
&lt;li&gt;Prices are on-demand at launch time and change daily. Billed cost for the four machines, which also ran a training benchmark, was $0.14 (RTX 5090), $0.11 (L40S, which also ran a model startup test), $0.08 (RTX A6000) and $0.43 (H100).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Launch a machine with &lt;code&gt;POST /api/v1/instances&lt;/code&gt; (&lt;code&gt;{"name": "...", "gpu_type": "L40S", "gpu_count": 1, "ssh_key_id": "...", "max_runtime_minutes": 40}&lt;/code&gt;), install torch and &lt;code&gt;sentence-transformers&lt;/code&gt; with uv.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python embed.py BAAI/bge-base-en-v1.5 512&lt;/code&gt; (the script above), then delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=embedding-throughput-per-dollar" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>embeddings</category>
      <category>rag</category>
      <category>gpu</category>
      <category>python</category>
    </item>
    <item>
      <title>Fresh Hugging Face download vs local cache: time to first token for a 7B model on a Runcrate L40S</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Thu, 29 Oct 2026 03:24:13 +0000</pubDate>
      <link>https://dev.to/runcrate/fresh-hugging-face-download-vs-local-cache-time-to-first-token-for-a-7b-model-on-a-runcrate-l40s-3no8</link>
      <guid>https://dev.to/runcrate/fresh-hugging-face-download-vs-local-cache-time-to-first-token-for-a-7b-model-on-a-runcrate-l40s-3no8</guid>
      <description>&lt;p&gt;How long does a GPU machine sit idle before a 7B model answers its first request, and how much of that is the download? I rented one L40S on Runcrate today (2026-10-29), timed every phase of starting Qwen2.5-7B-Instruct from a fresh Hugging Face download and from a copy already on the machine's disk, and deleted the machine when the numbers were saved.&lt;/p&gt;

&lt;p&gt;Short version: from a fresh download, the first token came 48 to 55 s after the Python process started. With the weights already on local disk it came after 18 to 19 s. The download was 30 s of that (15.2 GB at about 515 MB/s), and the part a cache cannot remove was the 14 s it took to read 15 GB from disk into GPU memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU:&lt;/strong&gt; one Runcrate L40S 48 GB (Des Moines, US, $0.54 per hour on demand at launch time), 12 vCPUs, 70 GB RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;code&gt;Qwen/Qwen2.5-7B-Instruct&lt;/code&gt;, bf16 safetensors, 15.24 GB on disk (4 shards).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software:&lt;/strong&gt; torch 2.8.0+cu128, transformers 4.57.1, huggingface-hub 0.36.2 with hf-xet 1.7.1 (the default download backend), Python 3.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One fresh process per measurement&lt;/strong&gt;, timing four phases: Python imports, &lt;code&gt;snapshot_download&lt;/code&gt; (a real download, or a cache check when the files are present), &lt;code&gt;from_pretrained(..., dtype=bfloat16, device_map="cuda")&lt;/code&gt;, and generating one token for a short chat prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six starts:&lt;/strong&gt; two fresh downloads with the defaults, one fresh download with &lt;code&gt;HF_XET_HIGH_PERFORMANCE=1&lt;/code&gt;, one from the local cache after evicting the files from the page cache, and two from the local cache right after a previous run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="n"&gt;T0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;huggingface_hub&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snapshot_download&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;T0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="n"&gt;MID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Qwen/Qwen2.5-7B-Instruct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snapshot_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_patterns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.safetensors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;download_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;load_to_gpu_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply_chat_template&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Say hello in five words.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
                              &lt;span class="n"&gt;add_generation_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_new_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;do_sample&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_token_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;T0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fresh start is &lt;code&gt;rm -rf $HF_HOME/hub/models--Qwen--Qwen2.5-7B-Instruct&lt;/code&gt; before the run. To evict a cached copy from RAM without root (&lt;code&gt;/proc/sys/vm/drop_caches&lt;/code&gt; was not writable in this container) I called &lt;code&gt;os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)&lt;/code&gt; on every blob file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Start&lt;/th&gt;
&lt;th&gt;Python imports (s)&lt;/th&gt;
&lt;th&gt;Download (s)&lt;/th&gt;
&lt;th&gt;Load to GPU (s)&lt;/th&gt;
&lt;th&gt;First token (s)&lt;/th&gt;
&lt;th&gt;Total (s)&lt;/th&gt;
&lt;th&gt;GPU cost of the wait&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fresh download, defaults (run 1)&lt;/td&gt;
&lt;td&gt;10.2&lt;/td&gt;
&lt;td&gt;29.6&lt;/td&gt;
&lt;td&gt;14.0&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;td&gt;54.5&lt;/td&gt;
&lt;td&gt;$0.0082&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fresh download, defaults (run 2)&lt;/td&gt;
&lt;td&gt;3.7&lt;/td&gt;
&lt;td&gt;29.6&lt;/td&gt;
&lt;td&gt;13.7&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;47.6&lt;/td&gt;
&lt;td&gt;$0.0071&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fresh download, HF_XET_HIGH_PERFORMANCE=1&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;32.5&lt;/td&gt;
&lt;td&gt;14.1&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;51.1&lt;/td&gt;
&lt;td&gt;$0.0077&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local disk cache, page cache evicted&lt;/td&gt;
&lt;td&gt;3.7&lt;/td&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;td&gt;13.9&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;18.3&lt;/td&gt;
&lt;td&gt;$0.0027&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local disk cache, in page cache (run 1)&lt;/td&gt;
&lt;td&gt;3.8&lt;/td&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;td&gt;14.7&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;td&gt;19.1&lt;/td&gt;
&lt;td&gt;$0.0029&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local disk cache, in page cache (run 2)&lt;/td&gt;
&lt;td&gt;3.8&lt;/td&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;td&gt;14.9&lt;/td&gt;
&lt;td&gt;0.53&lt;/td&gt;
&lt;td&gt;19.4&lt;/td&gt;
&lt;td&gt;$0.0029&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GPU cost of the wait is the total time at the L40S's hourly price.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The download was 60% of a fresh start.&lt;/strong&gt; 29.6 s in both default runs for 15.24 GB, about 515 MB/s or 4.1 Gbit/s. With the weights on disk the same phase took 0.1 s (a cache check).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HF_XET_HIGH_PERFORMANCE=1&lt;/code&gt; did not help here&lt;/strong&gt; (32.5 s against 29.6 s). At 4 Gbit/s on 12 vCPUs the default settings already used what this machine's network gave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loading to the GPU took 14 s every time, cached or not.&lt;/strong&gt; That is 15.24 GB at about 1.1 GB/s, and &lt;code&gt;dd&lt;/code&gt; read the largest shard at 1.0 GB/s from this disk, both right after eviction and right after a previous read. So on this machine the load is bound by disk reads, and keeping the file in RAM did not change it. I can't prove the eviction took effect, but the load time did not move either way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first token itself took about half a second&lt;/strong&gt; (0.50 to 0.57 s), including tokenizing the prompt and the first CUDA kernels. It is not where startup time goes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first Python import of the session took 10 s; later ones 4 s.&lt;/strong&gt; Bytecode compilation and cold library files cost 6 s once per machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If your machines start often, keep the weights on the machine (or on a disk you reattach) instead of pulling them from the Hub each time: here it cut time to first token from about 50 s to about 19 s.&lt;/li&gt;
&lt;li&gt;After that, the floor is reading the weights. 15 GB at 1 GB/s is 14 s, so a 70B model in bf16 (about 140 GB) would need roughly two minutes just to load at the same rate, and the download about 4.5 minutes more at 515 MB/s. Both are extrapolations, not measured.&lt;/li&gt;
&lt;li&gt;At $0.54 an hour, a fresh start's wait cost under a cent. The money matters less than the latency: a scale-to-zero endpoint that downloads on every cold start makes its first user wait close to a minute.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One machine, one region, one day, one model. Network speed to the Hub varies by region and time of day; disk speed varies by machine.&lt;/li&gt;
&lt;li&gt;My first script deleted the wrong cache directory (this image sets &lt;code&gt;HF_HOME=/workspace/hf&lt;/code&gt;), so its second "fresh" run was served from cache. I reran both fresh downloads and the evicted start on the same machine before it was deleted; those are the numbers above, and the discarded runs are kept with the raw logs.&lt;/li&gt;
&lt;li&gt;transformers &lt;code&gt;from_pretrained&lt;/code&gt; only. vLLM and other servers add their own startup work (CUDA graphs, KV cache allocation) on top of the load.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Launch an L40S with &lt;code&gt;POST /api/v1/instances&lt;/code&gt; (&lt;code&gt;{"name": "...", "gpu_type": "L40S", "gpu_count": 1, "ssh_key_id": "...", "max_runtime_minutes": 40}&lt;/code&gt;), install torch and &lt;code&gt;transformers==4.57.1 accelerate hf_xet&lt;/code&gt; with uv.&lt;/li&gt;
&lt;li&gt;Run the script above once after deleting the model's cache directory and once with it in place, then delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same machine also ran an embedding throughput test and a torch.compile training test; the whole run, about 12 minutes of billed L40S time, was billed $0.11.&lt;/p&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=model-weights-download-vs-cache-startup" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>huggingface</category>
      <category>llm</category>
      <category>gpu</category>
      <category>python</category>
    </item>
    <item>
      <title>Whisper large-v3 hours of audio per dollar on four GPUs: RTX 5090, L40S, RTX A6000 and H100, five setups measured</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:17:27 +0000</pubDate>
      <link>https://dev.to/runcrate/whisper-large-v3-hours-of-audio-per-dollar-on-four-gpus-rtx-5090-l40s-rtx-a6000-and-h100-five-3j7o</link>
      <guid>https://dev.to/runcrate/whisper-large-v3-hours-of-audio-per-dollar-on-four-gpus-rtx-5090-l40s-rtx-a6000-and-h100-five-3j7o</guid>
      <description>&lt;p&gt;How many hours of audio can Whisper large-v3 transcribe for a dollar? It depends on the GPU, but it depends even more on how you run it. I rented four GPUs on Runcrate today (2026-10-28), an RTX 5090, an L40S, an RTX A6000 and an H100 PCIe, ran the same 2.5 hours of English talks through five Whisper setups on each, measured speed and word error rate, and deleted every machine when the results were on disk.&lt;/p&gt;

&lt;p&gt;Short version: batched faster-whisper with the turbo model transcribed 315 hours of audio per hour on the RTX 5090, about 267 hours of audio per dollar, at a 3.1% word error rate. Plain sequential large-v3 with beam search, the way most tutorials start, was 6.6x slower than the same weights batched on that GPU and had a higher error rate (5.7% against 3.2%).&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPUs (one per machine), on-demand price at launch time:&lt;/strong&gt; RTX 5090 32 GB (Austin, US, $1.18 per hour), L40S 48 GB (Kansas City, US, $1.14), RTX A6000 48 GB (US Midwest, $0.66), H100 PCIe 80 GB (US Midwest, $3.86).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio:&lt;/strong&gt; the long-form TED talk test set from the distil-whisper project (&lt;code&gt;distil-whisper/tedlium-long-form&lt;/code&gt;): 11 full TED talks, 8,905 s (2 h 28 min) of 16 kHz English, with reference transcripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five setups, each in its own process:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;faster-whisper 1.2.1 (CTranslate2 4.8.4), &lt;code&gt;BatchedInferencePipeline&lt;/code&gt;, batch 16, with large-v3 in fp16, large-v3 in int8 (&lt;code&gt;int8_float16&lt;/code&gt;), and large-v3-turbo in fp16;&lt;/li&gt;
&lt;li&gt;transformers 4.57.1 &lt;code&gt;pipeline("automatic-speech-recognition")&lt;/code&gt;, large-v3 in fp16, 30 s chunks, batch 16, SDPA attention;&lt;/li&gt;
&lt;li&gt;faster-whisper large-v3 in fp16, plain &lt;code&gt;model.transcribe&lt;/code&gt; (sequential, beam 5), the setup most tutorials start with.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measured:&lt;/strong&gt; wall time to transcribe all 11 talks after a one-minute warm-up (model load excluded), speed as a multiple of real time, word error rate over all talks with Whisper's English text normalizer, and peak GPU memory from &lt;code&gt;nvidia-smi&lt;/code&gt; sampled every 0.5 s.&lt;/li&gt;
&lt;li&gt;torch 2.8.0+cu128, Python 3.12, on all four machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;

&lt;p&gt;Launch (the &lt;code&gt;instance_type_id&lt;/code&gt; comes from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"whisper-l40s","instance_type_id":"&amp;lt;id from /instances/types&amp;gt;",
       "ssh_key_id":"&amp;lt;your key id&amp;gt;","max_runtime_minutes":80}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 /opt/env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /opt/env/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;torch faster-whisper &lt;span class="s1"&gt;'transformers==4.57.1'&lt;/span&gt; accelerate soundfile pandas pyarrow jiwer &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cu128
&lt;span class="c"&gt;# CTranslate2 needs the cuBLAS and cuDNN that came with torch on its library path&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LD_LIBRARY_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import os, nvidia.cublas, nvidia.cudnn; print(os.path.join(nvidia.cublas.__path__[0],'lib')+':'+os.path.join(nvidia.cudnn.__path__[0],'lib'))"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:&lt;span class="nv"&gt;$LD_LIBRARY_PATH&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;CFG &lt;span class="k"&gt;in &lt;/span&gt;fw_large_v3_fp16_b16 fw_large_v3_int8_b16 fw_turbo_fp16_b16 hf_large_v3_fp16_b16 fw_large_v3_fp16_seq&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;python whisper_bench.py &lt;span class="nv"&gt;$CFG&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; results/&lt;span class="nv"&gt;$CFG&lt;/span&gt;.json
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two faster-whisper modes in &lt;code&gt;whisper_bench.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;faster_whisper&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WhisperModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BatchedInferencePipeline&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WhisperModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;large-v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compute_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;float16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# or "int8_float16"
# batched
&lt;/span&gt;&lt;span class="n"&gt;segs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BatchedInferencePipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# sequential
&lt;/span&gt;&lt;span class="n"&gt;segs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beam_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;segs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;DELETE /api/v1/instances/&amp;lt;id&amp;gt;&lt;/code&gt; and a check that the instance list is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;faster-whisper large-v3-turbo, fp16, batch 16&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;x realtime&lt;/th&gt;
&lt;th&gt;WER&lt;/th&gt;
&lt;th&gt;Peak GPU memory&lt;/th&gt;
&lt;th&gt;Audio hours per $&lt;/th&gt;
&lt;th&gt;$ per 1,000 audio hours&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.18&lt;/td&gt;
&lt;td&gt;315x&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;4.6 GiB&lt;/td&gt;
&lt;td&gt;267&lt;/td&gt;
&lt;td&gt;$3.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;1.14&lt;/td&gt;
&lt;td&gt;192x&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;4.5 GiB&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;$5.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;121x&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;4.4 GiB&lt;/td&gt;
&lt;td&gt;183&lt;/td&gt;
&lt;td&gt;$5.45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;196x&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;4.6 GiB&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;$19.65&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;faster-whisper large-v3, fp16, batch 16&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;x realtime&lt;/th&gt;
&lt;th&gt;WER&lt;/th&gt;
&lt;th&gt;Peak GPU memory&lt;/th&gt;
&lt;th&gt;Audio hours per $&lt;/th&gt;
&lt;th&gt;$ per 1,000 audio hours&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.18&lt;/td&gt;
&lt;td&gt;171x&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;td&gt;10.2 GiB&lt;/td&gt;
&lt;td&gt;145&lt;/td&gt;
&lt;td&gt;$6.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;1.14&lt;/td&gt;
&lt;td&gt;109x&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;td&gt;10.3 GiB&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;$10.43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;72x&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;td&gt;9.8 GiB&lt;/td&gt;
&lt;td&gt;109&lt;/td&gt;
&lt;td&gt;$9.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;118x&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;td&gt;10.0 GiB&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;$32.80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;faster-whisper large-v3, int8, batch 16&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;x realtime&lt;/th&gt;
&lt;th&gt;WER&lt;/th&gt;
&lt;th&gt;Peak GPU memory&lt;/th&gt;
&lt;th&gt;Audio hours per $&lt;/th&gt;
&lt;th&gt;$ per 1,000 audio hours&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.18&lt;/td&gt;
&lt;td&gt;172x&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;td&gt;8.4 GiB&lt;/td&gt;
&lt;td&gt;146&lt;/td&gt;
&lt;td&gt;$6.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;1.14&lt;/td&gt;
&lt;td&gt;109x&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;td&gt;8.3 GiB&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;$10.49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;73x&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;td&gt;8.1 GiB&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;td&gt;$9.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;119x&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;8.3 GiB&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;$32.36&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;transformers large-v3, fp16, batch 16&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;x realtime&lt;/th&gt;
&lt;th&gt;WER&lt;/th&gt;
&lt;th&gt;Peak GPU memory&lt;/th&gt;
&lt;th&gt;Audio hours per $&lt;/th&gt;
&lt;th&gt;$ per 1,000 audio hours&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.18&lt;/td&gt;
&lt;td&gt;47x&lt;/td&gt;
&lt;td&gt;2.9%&lt;/td&gt;
&lt;td&gt;27.9 GiB&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;$25.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;1.14&lt;/td&gt;
&lt;td&gt;23x&lt;/td&gt;
&lt;td&gt;2.9%&lt;/td&gt;
&lt;td&gt;27.6 GiB&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;$48.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;19x&lt;/td&gt;
&lt;td&gt;2.9%&lt;/td&gt;
&lt;td&gt;27.5 GiB&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;$34.92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;36x&lt;/td&gt;
&lt;td&gt;2.9%&lt;/td&gt;
&lt;td&gt;27.9 GiB&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;$106.93&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;faster-whisper large-v3, fp16, sequential, beam 5&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;x realtime&lt;/th&gt;
&lt;th&gt;WER&lt;/th&gt;
&lt;th&gt;Peak GPU memory&lt;/th&gt;
&lt;th&gt;Audio hours per $&lt;/th&gt;
&lt;th&gt;$ per 1,000 audio hours&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;1.18&lt;/td&gt;
&lt;td&gt;26x&lt;/td&gt;
&lt;td&gt;5.7%&lt;/td&gt;
&lt;td&gt;5.6 GiB&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;$45.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;1.14&lt;/td&gt;
&lt;td&gt;19x&lt;/td&gt;
&lt;td&gt;5.5%&lt;/td&gt;
&lt;td&gt;5.5 GiB&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;$60.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;10x&lt;/td&gt;
&lt;td&gt;6.0%&lt;/td&gt;
&lt;td&gt;5.4 GiB&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;$65.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;3.86&lt;/td&gt;
&lt;td&gt;19x&lt;/td&gt;
&lt;td&gt;5.5%&lt;/td&gt;
&lt;td&gt;5.6 GiB&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;$207.53&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Audio hours per $" is the speed divided by the hourly price, so it assumes the GPU is kept busy. The four machines were billed $2.23 in total for the whole run, including boot, installs and downloads: $0.30 (RTX 5090), $0.39 (L40S), $0.35 (RTX A6000) and $1.19 (H100 PCIe).&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The RTX 5090 was the fastest GPU here, ahead of the H100 PCIe&lt;/strong&gt;, in every setup: 171x against 118x for batched large-v3, 315x against 196x for turbo. At about a third of the hourly price it gave 4.3 to 5.2x the audio per dollar. The batched pipeline also does audio decoding, voice activity detection and feature extraction on the CPU, so the GPU is not the only limit; I did not profile where the time went on each machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;int8 gave no speedup on any of the four GPUs.&lt;/strong&gt; It saved about 1.8 GiB of memory and changed the error rate by 0.1 points at most. If memory is not your problem, fp16 is as good.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turbo was 1.7 to 1.8x faster than large-v3 at the same error rate&lt;/strong&gt; (3.1% against 3.2%) on this English set. Turbo keeps large-v3's encoder and cuts the decoder from 32 layers to 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential beam-5 transcription was the worst on both speed and accuracy.&lt;/strong&gt; 5.5 to 6.0% WER against 3.2% for the same weights batched, and 5.7 to 7.1x slower. On the L40S the first 250 characters of the first talk were identical in all three large-v3 setups, so the gap comes from later in the talks; long-form sequential decoding conditions each window on the text before it, which is a known source of repeated or dropped passages. I did not diff the transcripts line by line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;transformers had the best error rate (2.9%) and the highest cost.&lt;/strong&gt; 3 to 5x slower than batched faster-whisper with the same weights, and 28 GiB peak memory against 10 GiB.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For bulk English transcription, batched faster-whisper with turbo is the setup to start from, and a consumer-class 32 GB card did it at about $3.75 per 1,000 hours of audio here.&lt;/li&gt;
&lt;li&gt;Don't pay for int8 conversion or an H100 to make Whisper faster. Batching is the large win, and the model choice (turbo) the second.&lt;/li&gt;
&lt;li&gt;If the last 0.3 points of WER matter more than cost, the transformers pipeline was the most accurate in this test.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One run per setup, per machine, on one day. Treat differences under about 10% as noise.&lt;/li&gt;
&lt;li&gt;One English test set of TED talks: clean, single-speaker audio. Noisy calls, accents or other languages will give different error rates, and turbo is known to lose more accuracy than large-v3 on some languages.&lt;/li&gt;
&lt;li&gt;Peak memory comes from &lt;code&gt;nvidia-smi&lt;/code&gt;, which includes what the frameworks reserve, not only what they use.&lt;/li&gt;
&lt;li&gt;The CPU, disk and network differ between the four machines (6 to 20 vCPUs), and the batched pipeline uses the CPU, so part of the GPU-to-GPU difference may come from the rest of the machine.&lt;/li&gt;
&lt;li&gt;Prices are on-demand at launch time and move daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Pick a GPU from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt; and launch with the curl above, &lt;code&gt;max_runtime_minutes&lt;/code&gt; set as a safety net.&lt;/li&gt;
&lt;li&gt;Run the block above, copy &lt;code&gt;results/&lt;/code&gt; back, delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=whisper-audio-hours-per-dollar" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>whisper</category>
      <category>speechrecognition</category>
      <category>gpu</category>
      <category>python</category>
    </item>
    <item>
      <title>Full fine-tune vs LoRA vs QLoRA on one L40S: Qwen2.5-1.5B, memory, speed and held-out loss, measured</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:15:31 +0000</pubDate>
      <link>https://dev.to/runcrate/full-fine-tune-vs-lora-vs-qlora-on-one-l40s-qwen25-15b-memory-speed-and-held-out-loss-measured-1ibj</link>
      <guid>https://dev.to/runcrate/full-fine-tune-vs-lora-vs-qlora-on-one-l40s-qwen25-15b-memory-speed-and-held-out-loss-measured-1ibj</guid>
      <description>&lt;p&gt;"Use LoRA, it needs far less memory" and "use QLoRA if LoRA doesn't fit" are the standard advice for fine-tuning on one GPU. I wanted to see what each one costs on a small model, where the weights are not the problem, so I rented an L40S (48 GB) on Runcrate today (2026-10-28) and fine-tuned Qwen2.5-1.5B-Instruct three ways on the same data for the same number of steps. Two machines, both deleted when the results were on disk.&lt;/p&gt;

&lt;p&gt;Short version: full fine-tuning needed 36.9 GiB at micro-batch 4 and ran out of memory at micro-batch 8. LoRA needed 14.5 GiB, trained 1.4x faster and reached a slightly lower held-out loss. QLoRA was slower than both and used more memory than LoRA, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU:&lt;/strong&gt; one L40S (48 GB, 44.4 GiB usable), Kansas City, US, $1.14 per hour on demand at launch time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Qwen/Qwen2.5-1.5B-Instruct (1.54 billion parameters).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data:&lt;/strong&gt; yahma/alpaca-cleaned, shuffled with a fixed seed: 4,000 examples for training (753,486 tokens), the next 300 held out. Qwen chat template, loss on the answer tokens only, at most 512 tokens per example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training:&lt;/strong&gt; one epoch, 250 optimizer steps of 16 examples each, cosine schedule with 10 warm-up steps, gradient clipping at 1.0, AdamW, no gradient checkpointing. A plain PyTorch loop, the same for all three:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;full:&lt;/strong&gt; fp32 master weights, bf16 autocast, learning rate 1e-5;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoRA:&lt;/strong&gt; bf16 base, rank 16, alpha 32, dropout 0.05 on all attention and MLP projections (18.5 M trainable parameters, 1.2% of the model), learning rate 2e-4;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QLoRA:&lt;/strong&gt; the same LoRA on a 4-bit NF4 base (double quantization, bf16 compute, bitsandbytes 0.50.2), prepared with peft's &lt;code&gt;prepare_model_for_kbit_training&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two runs:&lt;/strong&gt; micro-batch 8 with 2 accumulation steps first. Full fine-tuning ran out of memory there, so a second machine ran all three at micro-batch 4 with 4 accumulation steps (still 16 per step).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measured:&lt;/strong&gt; training wall time (excluding model load and evaluation), tokens per second, peak memory from &lt;code&gt;torch.cuda.max_memory_allocated&lt;/code&gt;, loss on the 300 held-out examples before and after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software:&lt;/strong&gt; torch 2.8.0+cu128, transformers 4.57.1, peft 0.17.1, accelerate 1.15.0, Python 3.12.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;

&lt;p&gt;Launch (the &lt;code&gt;instance_type_id&lt;/code&gt; comes from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"ft-l40s","instance_type_id":"&amp;lt;id from /instances/types&amp;gt;",
       "ssh_key_id":"&amp;lt;your key id&amp;gt;","max_runtime_minutes":60}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 /opt/env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /opt/env/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;torch &lt;span class="s1"&gt;'transformers==4.57.1'&lt;/span&gt; &lt;span class="s1"&gt;'peft==0.17.1'&lt;/span&gt; bitsandbytes accelerate &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cu128
&lt;span class="k"&gt;for &lt;/span&gt;CFG &lt;span class="k"&gt;in &lt;/span&gt;full lora qlora&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nv"&gt;MB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nv"&gt;ACC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 python ft_bench.py &lt;span class="nv"&gt;$CFG&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; results/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CFG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;_mb4.json&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parts of &lt;code&gt;ft_bench.py&lt;/code&gt; that differ between the three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;CFG&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="mf"&gt;1e-5&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;CFG&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qlora&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BitsAndBytesConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;load_in_4bit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bnb_4bit_quant_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nf4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="n"&gt;bnb_4bit_use_double_quant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bnb_4bit_compute_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantization_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_map&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prepare_model_for_kbit_training&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_gradient_checkpointing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_peft_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LoraConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lora_alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lora_dropout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAUSAL_LM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;target_modules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q_proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k_proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v_proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o_proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gate_proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;up_proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;down_proj&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requires_grad&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;2e-4&lt;/span&gt;
&lt;span class="n"&gt;opt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;optim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AdamW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight_decay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# training step: with torch.autocast("cuda", dtype=torch.bfloat16): loss = model(...).loss
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;DELETE /api/v1/instances/&amp;lt;id&amp;gt;&lt;/code&gt; and a check that the instance list is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Micro-batch x accumulation&lt;/th&gt;
&lt;th&gt;Trainable&lt;/th&gt;
&lt;th&gt;Train time&lt;/th&gt;
&lt;th&gt;Tokens/s&lt;/th&gt;
&lt;th&gt;Peak memory (PyTorch)&lt;/th&gt;
&lt;th&gt;Held-out loss&lt;/th&gt;
&lt;th&gt;GPU cost of training&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;full&lt;/td&gt;
&lt;td&gt;4 x 4&lt;/td&gt;
&lt;td&gt;1,543.7 M&lt;/td&gt;
&lt;td&gt;210 s&lt;/td&gt;
&lt;td&gt;3,595&lt;/td&gt;
&lt;td&gt;36.9 GiB&lt;/td&gt;
&lt;td&gt;1.103 to 1.065&lt;/td&gt;
&lt;td&gt;$0.066&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoRA&lt;/td&gt;
&lt;td&gt;4 x 4&lt;/td&gt;
&lt;td&gt;18.5 M&lt;/td&gt;
&lt;td&gt;152 s&lt;/td&gt;
&lt;td&gt;4,970&lt;/td&gt;
&lt;td&gt;14.5 GiB&lt;/td&gt;
&lt;td&gt;1.102 to 1.058&lt;/td&gt;
&lt;td&gt;$0.048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QLoRA&lt;/td&gt;
&lt;td&gt;4 x 4&lt;/td&gt;
&lt;td&gt;18.5 M&lt;/td&gt;
&lt;td&gt;215 s&lt;/td&gt;
&lt;td&gt;3,507&lt;/td&gt;
&lt;td&gt;16.5 GiB&lt;/td&gt;
&lt;td&gt;1.144 to 1.090&lt;/td&gt;
&lt;td&gt;$0.068&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;full&lt;/td&gt;
&lt;td&gt;8 x 2&lt;/td&gt;
&lt;td&gt;1,543.7 M&lt;/td&gt;
&lt;td&gt;out of memory&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoRA&lt;/td&gt;
&lt;td&gt;8 x 2&lt;/td&gt;
&lt;td&gt;18.5 M&lt;/td&gt;
&lt;td&gt;161 s&lt;/td&gt;
&lt;td&gt;4,680&lt;/td&gt;
&lt;td&gt;25.8 GiB&lt;/td&gt;
&lt;td&gt;1.102 to 1.056&lt;/td&gt;
&lt;td&gt;$0.051&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QLoRA&lt;/td&gt;
&lt;td&gt;8 x 2&lt;/td&gt;
&lt;td&gt;18.5 M&lt;/td&gt;
&lt;td&gt;237 s&lt;/td&gt;
&lt;td&gt;3,184&lt;/td&gt;
&lt;td&gt;30.7 GiB&lt;/td&gt;
&lt;td&gt;1.144 to 1.089&lt;/td&gt;
&lt;td&gt;$0.075&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cost column is training time only, at $1.14 per hour. The two machines were billed $0.18 and $0.24 in total, including boot, installs, downloads, evaluation and the run that failed.&lt;/p&gt;

&lt;p&gt;All three models gave nearly the same answers to three fixed prompts afterwards. For "Rewrite this sentence in the passive voice: The cat chased the mouse." all three answered "The mouse was chased by the cat."&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Full fine-tuning of a 1.5B model does not fit in 48 GB at micro-batch 8&lt;/strong&gt;, without gradient checkpointing. fp32 weights, fp32 gradients and the two AdamW states are 16 bytes per parameter, about 23 GiB before a single activation. At micro-batch 4 it peaked at 36.9 GiB. The OOM came in the backward pass with 39.6 GiB already allocated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QLoRA saved nothing here.&lt;/strong&gt; On a 1.5B model the bf16 weights are only about 3 GiB, so quantizing them to 4 bits can save at most about 2 GiB, and the activations at these batch sizes are much larger than that. QLoRA's peak was 2 GiB higher than LoRA's at micro-batch 4 and 4.9 GiB higher at micro-batch 8. peft's &lt;code&gt;prepare_model_for_kbit_training&lt;/code&gt; casts every layer that is not quantized to fp32, including Qwen's 151,936-token embedding, and the 4-bit layers are dequantized on every forward and backward pass; my guess is that these two account for the extra memory and for QLoRA being the slowest of the three, but I did not profile it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantizing the base model cost almost as much loss as the fine-tune removed.&lt;/strong&gt; QLoRA's held-out loss before training was 1.144, against 1.102 for the bf16 model, a 0.042 penalty. Training removed 0.054, about as much as LoRA removed (0.044), so QLoRA finished at 1.090, worse than where LoRA and full fine-tuning finished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoRA matched full fine-tuning on held-out loss&lt;/strong&gt; (1.058 against 1.065) with 1.2% of the parameters trainable, while training 1.4x faster and using 40% of the memory. On this data and at these learning rates, full fine-tuning bought nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Halving the micro-batch made LoRA faster, not slower&lt;/strong&gt; (152 s against 161 s) and cut its peak memory from 25.8 to 14.5 GiB. The examples vary in length and each micro-batch is padded to its longest one, so smaller micro-batches probably waste less compute on padding.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For a model this size on a 48 GB card, plain LoRA in bf16 is the default to beat: least memory, fastest, and no worse on held-out loss than full fine-tuning here.&lt;/li&gt;
&lt;li&gt;QLoRA is a tool for models whose bf16 weights don't fit. Below a few billion parameters, the activations dominate, and the 4-bit base costs speed and starting quality for no memory back.&lt;/li&gt;
&lt;li&gt;If you need full fine-tuning, micro-batch and gradient checkpointing decide whether it fits; check the peak memory on a few steps before you rent a bigger GPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One run per setting, one seed, one GPU type, one day. Treat differences under about 10% as noise, and the loss differences between full and LoRA as a tie.&lt;/li&gt;
&lt;li&gt;One epoch on 4,000 Alpaca examples, starting from an instruct model that already follows instructions, so the loss moves only a little. A harder task or more data could favour full fine-tuning.&lt;/li&gt;
&lt;li&gt;The learning rates (1e-5 for full, 2e-4 for LoRA) are common defaults, not tuned. Full fine-tuning at a higher rate might do better.&lt;/li&gt;
&lt;li&gt;No gradient checkpointing, no fused or 8-bit optimizers, no FlashAttention 2 (PyTorch SDPA). Each would change the memory numbers.&lt;/li&gt;
&lt;li&gt;Price is the on-demand price at launch time and moves daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Pick an L40S from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt; and launch with the curl above, &lt;code&gt;max_runtime_minutes&lt;/code&gt; set as a safety net.&lt;/li&gt;
&lt;li&gt;Run the block above, copy &lt;code&gt;results/&lt;/code&gt; back, delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=lora-vs-full-finetune" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>llm</category>
      <category>pytorch</category>
      <category>gpu</category>
    </item>
    <item>
      <title>Ollama Q4 vs vLLM bf16 on a 24 GB A10: 3x the single-user speed, 1.2 points of GSM8K, and where vLLM still wins</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:14:18 +0000</pubDate>
      <link>https://dev.to/runcrate/ollama-q4-vs-vllm-bf16-on-a-24-gb-a10-3x-the-single-user-speed-12-points-of-gsm8k-and-where-3ch4</link>
      <guid>https://dev.to/runcrate/ollama-q4-vs-vllm-bf16-on-a-24-gb-a10-3x-the-single-user-speed-12-points-of-gsm8k-and-where-3ch4</guid>
      <description>&lt;p&gt;If you have one 24 GB GPU and a 7B model to serve, the usual advice is "use vLLM for throughput, use Ollama (llama.cpp) for convenience". I wanted numbers for that on the same card, the same model and the same requests, so I rented one NVIDIA A10 (24 GB) on Runcrate today (2026-10-28), served Qwen2.5-7B-Instruct three ways, and deleted the machine when the results were on disk.&lt;/p&gt;

&lt;p&gt;Short version: on this card, Ollama with the Q4_K_M build was faster than vLLM in bf16 at every load I tried, from 1 to 16 users at once, and cost less per token. Q4 scored 90.0% on 250 GSM8K questions against 91.2% for bf16. vLLM kept the better time to first token once more than one user was waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU:&lt;/strong&gt; one NVIDIA A10, 24 GB (23,028 MiB usable, 150 W limit), Dulles, US, $1.68 per hour on demand at launch time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Qwen2.5-7B-Instruct, three ways:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama 0.40.12, &lt;code&gt;qwen2.5:7b-instruct-q4_K_M&lt;/code&gt;&lt;/strong&gt; (GGUF, 4-bit, llama.cpp runner),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama 0.40.12, &lt;code&gt;qwen2.5:7b-instruct-q8_0&lt;/code&gt;&lt;/strong&gt; (GGUF, 8-bit),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vLLM 0.11.0, &lt;code&gt;Qwen/Qwen2.5-7B-Instruct&lt;/code&gt; in bf16&lt;/strong&gt; (torch 2.8.0+cu128, transformers 4.57.6), &lt;code&gt;--max-model-len 4096 --gpu-memory-utilization 0.92&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama settings:&lt;/strong&gt; &lt;code&gt;OLLAMA_NUM_PARALLEL=16 OLLAMA_CONTEXT_LENGTH=4096&lt;/code&gt;, so it can run 16 requests at once with a 4,096-token context each. All 29 layers were on the GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client:&lt;/strong&gt; one Python script for all three servers, through each server's OpenAI-compatible &lt;code&gt;/v1/chat/completions&lt;/code&gt;, streamed. 32 fixed "explain X to a new engineer" prompts, temperature 0, &lt;code&gt;max_tokens&lt;/code&gt; 256. Every request in every run produced the full 256 tokens, so all three servers did the same amount of work. 1, 4 and 16 users at once (12, 32 and 96 requests), one warm-up request first. The client ran on the same machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality check:&lt;/strong&gt; the first 250 questions of the GSM8K test set, zero-shot, greedy, up to 512 tokens, 16 at a time, answer read from the last number.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;

&lt;p&gt;Launch (the &lt;code&gt;instance_type_id&lt;/code&gt; comes from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"ollama-a10","instance_type_id":"&amp;lt;id from /instances/types&amp;gt;",
       "ssh_key_id":"&amp;lt;your key id&amp;gt;","max_runtime_minutes":90}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the machine, Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://ollama.com/install.sh | sh
systemctl stop ollama   &lt;span class="c"&gt;# run our own server with our settings&lt;/span&gt;
&lt;span class="nv"&gt;OLLAMA_NUM_PARALLEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16 &lt;span class="nv"&gt;OLLAMA_CONTEXT_LENGTH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4096 &lt;span class="nv"&gt;OLLAMA_KEEP_ALIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt; ollama serve &amp;amp;
ollama pull qwen2.5:7b-instruct-q4_K_M
python client.py http://localhost:11434/v1 qwen2.5:7b-instruct-q4_K_M speed 16 96
python client.py http://localhost:11434/v1 qwen2.5:7b-instruct-q4_K_M gsm8k
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vLLM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 /opt/env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /opt/env/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;vllm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.11.0 &lt;span class="s1"&gt;'transformers&amp;lt;4.58'&lt;/span&gt; openai pandas pyarrow &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cu128
vllm serve Qwen/Qwen2.5-7B-Instruct &lt;span class="nt"&gt;--dtype&lt;/span&gt; bfloat16 &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 4096 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.92 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &amp;amp;
python client.py http://localhost:8000/v1 Qwen/Qwen2.5-7B-Instruct speed 16 96
python client.py http://localhost:8000/v1 Qwen/Qwen2.5-7B-Instruct gsm8k
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The heart of &lt;code&gt;client.py&lt;/code&gt;, per request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)}],&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stream_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# time to first content chunk, then usage.completion_tokens at the end
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both servers reported &lt;code&gt;usage&lt;/code&gt; in the stream, so token counts come from the servers, not from counting chunks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Output tok/s&lt;/th&gt;
&lt;th&gt;Per-user tok/s (median)&lt;/th&gt;
&lt;th&gt;Median TTFT&lt;/th&gt;
&lt;th&gt;p95 TTFT&lt;/th&gt;
&lt;th&gt;$ per 1M output tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ollama Q4_K_M&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;35 ms&lt;/td&gt;
&lt;td&gt;40 ms&lt;/td&gt;
&lt;td&gt;$5.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama Q4_K_M&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;239&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;110 ms&lt;/td&gt;
&lt;td&gt;136 ms&lt;/td&gt;
&lt;td&gt;$1.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama Q4_K_M&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;618&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;405 ms&lt;/td&gt;
&lt;td&gt;483 ms&lt;/td&gt;
&lt;td&gt;$0.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama Q8_0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;42 ms&lt;/td&gt;
&lt;td&gt;44 ms&lt;/td&gt;
&lt;td&gt;$8.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama Q8_0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;197&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;128 ms&lt;/td&gt;
&lt;td&gt;152 ms&lt;/td&gt;
&lt;td&gt;$2.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama Q8_0&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;484&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;456 ms&lt;/td&gt;
&lt;td&gt;511 ms&lt;/td&gt;
&lt;td&gt;$0.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM bf16&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;47 ms&lt;/td&gt;
&lt;td&gt;48 ms&lt;/td&gt;
&lt;td&gt;$15.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM bf16&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;72 ms&lt;/td&gt;
&lt;td&gt;100 ms&lt;/td&gt;
&lt;td&gt;$3.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM bf16&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;449&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;106 ms&lt;/td&gt;
&lt;td&gt;173 ms&lt;/td&gt;
&lt;td&gt;$1.04&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dollar column is $1.68 per hour divided by the output tokens per hour at that load, so it assumes the GPU stays that busy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;GPU memory in use&lt;/th&gt;
&lt;th&gt;GSM8K (250, greedy)&lt;/th&gt;
&lt;th&gt;GSM8K wall time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ollama Q4_K_M&lt;/td&gt;
&lt;td&gt;8.4 GiB&lt;/td&gt;
&lt;td&gt;90.0% (225/250)&lt;/td&gt;
&lt;td&gt;104 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama Q8_0&lt;/td&gt;
&lt;td&gt;11.3 GiB&lt;/td&gt;
&lt;td&gt;90.4% (226/250)&lt;/td&gt;
&lt;td&gt;140 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM bf16&lt;/td&gt;
&lt;td&gt;21.3 GiB&lt;/td&gt;
&lt;td&gt;91.2% (228/250)&lt;/td&gt;
&lt;td&gt;125 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vLLM's memory figure is what it reserves up front (&lt;code&gt;--gpu-memory-utilization 0.92&lt;/code&gt;), not what it needed: 4.61 GiB of that was KV cache, room for 86,352 tokens. Ollama allocated 3.5 GiB of KV cache for 16 slots of 4,096 tokens.&lt;/p&gt;

&lt;p&gt;Other timings from the same run: &lt;code&gt;ollama pull&lt;/code&gt; took 22 s for Q4_K_M (4.7 GB) and 34 s for Q8_0, and the first request after a pull loaded the model in under 3 s. vLLM took 61 s from &lt;code&gt;vllm serve&lt;/code&gt; to answering, with the weights already downloaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A single user got 3x the speed from Q4.&lt;/strong&gt; 89 tokens per second against 30. Generating one token means reading every weight once, so a model that is 4.7 GB instead of 15 GB is read about three times faster on the same memory bandwidth. The A10's bandwidth (about 600 GB/s) is the limit here, not its compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vLLM's per-user speed did not drop at all from 1 to 16 users&lt;/strong&gt; (30, 30, 28 tokens per second), while Ollama Q4's went from 90 to 41. vLLM batches the 16 streams into one pass over the weights, so 16 users cost almost the same as one. Ollama also batches, but each extra user cost it more. At 16 users Ollama Q4 still led on total throughput (618 against 449 tokens per second), but the gap had closed from 3x to 1.4x. I did not test past 16 users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vLLM answered first under load.&lt;/strong&gt; At 16 users the median time to first token was 106 ms for vLLM and 405 ms for Ollama Q4. If your users notice the wait before the first word more than the speed after it, that matters more than the throughput column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q8 was not a free middle ground.&lt;/strong&gt; It was slower than Q4 at every load and scored one more GSM8K question (226 against 225). Against bf16, Q4 and bf16 disagreed on 13 of 250 questions: bf16 got 8 of them right and Q4 got 5. A 1.2-point gap on 250 questions is within what reruns could move.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On a 24 GB card with a 7B model and up to 16 concurrent users, a 4-bit GGUF under Ollama gave more tokens per dollar than bf16 under vLLM, at a small cost in GSM8K accuracy.&lt;/li&gt;
&lt;li&gt;This is not Ollama against vLLM in general. vLLM can also serve 4-bit and 8-bit models (AWQ, GPTQ, FP8 on newer cards), and that comparison would be fairer to it. Here vLLM ran the full bf16 weights because that is what most people start with.&lt;/li&gt;
&lt;li&gt;If you serve many users at once and care about time to first token, vLLM's behaviour under load (flat per-user speed, low TTFT) is the reason to choose it. Measure at your own concurrency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One run per setting, on one machine, on one day. Treat differences under about 10% as noise.&lt;/li&gt;
&lt;li&gt;256 output tokens and short prompts (about 30 tokens). Long prompts would shift the picture towards prefill speed, where the two engines differ in other ways.&lt;/li&gt;
&lt;li&gt;GSM8K is one narrow test of quality. Quantization can hurt other tasks more or less.&lt;/li&gt;
&lt;li&gt;Price is the on-demand price at launch time and moves daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Pick an A10 (or any 24 GB card) from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt; and launch with the curl above, &lt;code&gt;max_runtime_minutes&lt;/code&gt; set as a safety net.&lt;/li&gt;
&lt;li&gt;Run the blocks above, copy the results back, delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole run, including both installs, the model downloads, the speed tests and three GSM8K passes, was billed $0.54 (about 19 minutes at $1.68 per hour; billing starts when the machine reports running, 5 minutes after the create call).&lt;/p&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=ollama-vs-vllm-24gb" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ollama</category>
      <category>vllm</category>
      <category>gpu</category>
    </item>
    <item>
      <title>SDXL images per dollar on four GPUs: RTX A6000, L40S, RTX 5090 and H100, eager vs torch.compile</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:28:10 +0000</pubDate>
      <link>https://dev.to/runcrate/sdxl-images-per-dollar-on-four-gpus-rtx-a6000-l40s-rtx-5090-and-h100-eager-vs-torchcompile-16no</link>
      <guid>https://dev.to/runcrate/sdxl-images-per-dollar-on-four-gpus-rtx-a6000-l40s-rtx-5090-and-h100-eager-vs-torchcompile-16no</guid>
      <description>&lt;p&gt;How many SDXL images does a dollar buy on a rented GPU? It depends on the card, the batch size and whether you pay for &lt;code&gt;torch.compile&lt;/code&gt;, so I measured it. Today (2026-10-27) I rented four single-GPU machines on Runcrate, ran the same SDXL script on each, and terminated them as soon as the numbers were on disk.&lt;/p&gt;

&lt;p&gt;Short version: an RTX A6000 made 1,000 images for $0.91, an RTX 5090 for $0.93, an L40S for $1.01 and an H100 for $1.80. The H100 was the fastest by far, 1.67 s per image against 4.94 s on the A6000, and the most expensive per image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; &lt;code&gt;stabilityai/stable-diffusion-xl-base-1.0&lt;/code&gt;, fp16 weights, with the &lt;code&gt;madebyollin/sdxl-vae-fp16-fix&lt;/code&gt; VAE. Base model only, no refiner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image:&lt;/strong&gt; 1024 x 1024, 30 steps, the pipeline's default scheduler and guidance, one fixed prompt ("a photo of an astronaut riding a horse on a beach at sunset, detailed, 35mm").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software:&lt;/strong&gt; diffusers 0.42.1, torch 2.8.0+cu128, Python 3.12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four configurations per GPU:&lt;/strong&gt; batch 1 and batch 4 (&lt;code&gt;num_images_per_prompt&lt;/code&gt;), each in eager mode and with &lt;code&gt;torch.compile(pipe.unet, fullgraph=True)&lt;/code&gt; plus channels-last memory format. One warm-up generation before each timed run; 8 images timed at batch 1, 16 at batch 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machines&lt;/strong&gt; (on-demand price at launch time):&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runcrate GPU&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;48 GB&lt;/td&gt;
&lt;td&gt;US Midwest&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;48 GB&lt;/td&gt;
&lt;td&gt;Des Moines, US&lt;/td&gt;
&lt;td&gt;1.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;td&gt;Gdansk, PL&lt;/td&gt;
&lt;td&gt;1.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;80 GB&lt;/td&gt;
&lt;td&gt;Des Moines, US&lt;/td&gt;
&lt;td&gt;3.88&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cost per 1,000 images is &lt;code&gt;hourly price / images per hour x 1,000&lt;/code&gt;, with the GPU kept busy. It does not include setup time; that is below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;

&lt;p&gt;Launch (one call per GPU; the &lt;code&gt;instance_type_id&lt;/code&gt; comes from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"sdxl-a6000","instance_type_id":"&amp;lt;id from /instances/types&amp;gt;",
       "ssh_key_id":"&amp;lt;your key id&amp;gt;","max_runtime_minutes":50}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 /opt/env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /opt/env/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;torch diffusers transformers accelerate safetensors &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cu128
python sdxl_bench.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core of &lt;code&gt;sdxl_bench.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;vae&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoencoderKL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;madebyollin/sdxl-vae-fp16-fix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StableDiffusionXLPipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stabilityai/stable-diffusion-xl-base-1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vae&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;vae&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;torch_dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fp16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_safetensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_batches&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_inference_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_images_per_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# warm-up
&lt;/span&gt;    &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_batches&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_inference_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_images_per_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n_batches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# seconds per image
&lt;/span&gt;
&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                  &lt;span class="c1"&gt;# eager
&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channels_last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fullgraph&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                  &lt;span class="c1"&gt;# compiled
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;DELETE /api/v1/instances/&amp;lt;id&amp;gt;&lt;/code&gt; and a check that the instance list is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Seconds per image&lt;/th&gt;
&lt;th&gt;Images per hour&lt;/th&gt;
&lt;th&gt;$ per 1,000 images&lt;/th&gt;
&lt;th&gt;Peak VRAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;eager, batch 1&lt;/td&gt;
&lt;td&gt;5.79&lt;/td&gt;
&lt;td&gt;622&lt;/td&gt;
&lt;td&gt;1.06&lt;/td&gt;
&lt;td&gt;9.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;eager, batch 4&lt;/td&gt;
&lt;td&gt;5.65&lt;/td&gt;
&lt;td&gt;637&lt;/td&gt;
&lt;td&gt;1.04&lt;/td&gt;
&lt;td&gt;14.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;compiled, batch 1&lt;/td&gt;
&lt;td&gt;4.94&lt;/td&gt;
&lt;td&gt;728&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.91&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;compiled, batch 4&lt;/td&gt;
&lt;td&gt;5.13&lt;/td&gt;
&lt;td&gt;701&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;14.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;eager, batch 1&lt;/td&gt;
&lt;td&gt;3.48&lt;/td&gt;
&lt;td&gt;1,034&lt;/td&gt;
&lt;td&gt;1.10&lt;/td&gt;
&lt;td&gt;9.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;eager, batch 4&lt;/td&gt;
&lt;td&gt;3.39&lt;/td&gt;
&lt;td&gt;1,064&lt;/td&gt;
&lt;td&gt;1.07&lt;/td&gt;
&lt;td&gt;14.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;compiled, batch 1&lt;/td&gt;
&lt;td&gt;3.21&lt;/td&gt;
&lt;td&gt;1,121&lt;/td&gt;
&lt;td&gt;1.02&lt;/td&gt;
&lt;td&gt;9.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;compiled, batch 4&lt;/td&gt;
&lt;td&gt;3.19&lt;/td&gt;
&lt;td&gt;1,130&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.01&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;eager, batch 1&lt;/td&gt;
&lt;td&gt;2.77&lt;/td&gt;
&lt;td&gt;1,301&lt;/td&gt;
&lt;td&gt;1.02&lt;/td&gt;
&lt;td&gt;9.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;eager, batch 4&lt;/td&gt;
&lt;td&gt;2.66&lt;/td&gt;
&lt;td&gt;1,355&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;td&gt;14.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;compiled, batch 1&lt;/td&gt;
&lt;td&gt;2.53&lt;/td&gt;
&lt;td&gt;1,424&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.93&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;compiled, batch 4&lt;/td&gt;
&lt;td&gt;2.53&lt;/td&gt;
&lt;td&gt;1,421&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;14.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;eager, batch 1&lt;/td&gt;
&lt;td&gt;2.25&lt;/td&gt;
&lt;td&gt;1,603&lt;/td&gt;
&lt;td&gt;2.42&lt;/td&gt;
&lt;td&gt;9.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;eager, batch 4&lt;/td&gt;
&lt;td&gt;1.94&lt;/td&gt;
&lt;td&gt;1,853&lt;/td&gt;
&lt;td&gt;2.09&lt;/td&gt;
&lt;td&gt;14.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;compiled, batch 1&lt;/td&gt;
&lt;td&gt;1.80&lt;/td&gt;
&lt;td&gt;2,003&lt;/td&gt;
&lt;td&gt;1.94&lt;/td&gt;
&lt;td&gt;9.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;compiled, batch 4&lt;/td&gt;
&lt;td&gt;1.67&lt;/td&gt;
&lt;td&gt;2,156&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14.6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What &lt;code&gt;torch.compile&lt;/code&gt; costs up front (the first compiled call, measured on the machine) and how many images it takes to earn that back at batch 1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Compile, batch 1&lt;/th&gt;
&lt;th&gt;Compile, batch 4&lt;/th&gt;
&lt;th&gt;Saved per image (batch 1)&lt;/th&gt;
&lt;th&gt;Break-even&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;188 s&lt;/td&gt;
&lt;td&gt;255 s&lt;/td&gt;
&lt;td&gt;0.85 s&lt;/td&gt;
&lt;td&gt;about 220 images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;82 s&lt;/td&gt;
&lt;td&gt;121 s&lt;/td&gt;
&lt;td&gt;0.27 s&lt;/td&gt;
&lt;td&gt;about 300 images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;96 s&lt;/td&gt;
&lt;td&gt;133 s&lt;/td&gt;
&lt;td&gt;0.24 s&lt;/td&gt;
&lt;td&gt;about 400 images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;94 s&lt;/td&gt;
&lt;td&gt;130 s&lt;/td&gt;
&lt;td&gt;0.45 s&lt;/td&gt;
&lt;td&gt;about 210 images&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Batch 4 recompiles (new input shape), so a server that mixes batch sizes pays both.&lt;/p&gt;

&lt;p&gt;Setup on each machine, before the first image:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Launch to SSH&lt;/th&gt;
&lt;th&gt;diffusers install&lt;/th&gt;
&lt;th&gt;Model download (about 7 GB)&lt;/th&gt;
&lt;th&gt;Pipeline load&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;288 s&lt;/td&gt;
&lt;td&gt;29 s&lt;/td&gt;
&lt;td&gt;73 s&lt;/td&gt;
&lt;td&gt;3.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;190 s&lt;/td&gt;
&lt;td&gt;under 1 s&lt;/td&gt;
&lt;td&gt;24 s&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 5090&lt;/td&gt;
&lt;td&gt;122 s&lt;/td&gt;
&lt;td&gt;15 s&lt;/td&gt;
&lt;td&gt;25 s&lt;/td&gt;
&lt;td&gt;2.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;307 s&lt;/td&gt;
&lt;td&gt;7 s&lt;/td&gt;
&lt;td&gt;27 s&lt;/td&gt;
&lt;td&gt;2.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Three of the four cards were within 11% of each other per image, and the H100 cost about twice as much per image.&lt;/strong&gt; The A6000, RTX 5090 and L40S landed between $0.91 and $1.01 per 1,000 images. The H100 was 3x faster than the A6000 and cost 5.9x more per hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch 4 barely helped, except on the H100.&lt;/strong&gt; SDXL at 1024 x 1024 already fills these GPUs with one image: batch 4 changed throughput by -4% to +4% on the A6000, L40S and 5090. Only the H100 gained (16% eager, 8% compiled). Batching mostly adds latency for the person waiting for the first image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;torch.compile&lt;/code&gt; cut the time per image by 8% to 20%, if you generate a few hundred images per process.&lt;/strong&gt; The biggest gain was on the A6000 (5.79 to 4.94 s), which also had the slowest compile (188 s). For a one-off batch of 50 images, eager mode is the better deal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peak memory was 9.0 GB at batch 1 and 14.6 GB at batch 4,&lt;/strong&gt; so SDXL base fits on any of these cards with room to spare. VRAM size did not matter for this workload; compute did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model download varied 3x between machines&lt;/strong&gt; (24 s to 73 s for the same 7 GB). For short jobs, keep the weights on a volume.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One run per GPU, 8 to 16 timed images per configuration. Treat differences under about 5% as noise.&lt;/li&gt;
&lt;li&gt;One prompt, one resolution, one scheduler, 30 steps. Fewer steps, a distilled model (SDXL Turbo, Lightning) or a different scheduler change the absolute numbers; rerun before relying on them.&lt;/li&gt;
&lt;li&gt;No refiner, no ControlNet, no LoRA, no xFormers or other attention kernels beyond PyTorch's built-in SDPA, no TensorRT.&lt;/li&gt;
&lt;li&gt;Prices are the on-demand prices at launch time on 2026-10-27 and move daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Pick a GPU from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt; and launch with the curl above, &lt;code&gt;max_runtime_minutes&lt;/code&gt; set as a safety net.&lt;/li&gt;
&lt;li&gt;Run the block above, copy &lt;code&gt;results/&lt;/code&gt; back, delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The four machines, including setup time and compile warm-ups, were billed $1.02 together ($0.17 for the A6000, $0.15 for the L40S, $0.19 for the RTX 5090, $0.50 for the H100).&lt;/p&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=sdxl-images-per-dollar" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stablediffusion</category>
      <category>gpu</category>
      <category>pytorch</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>vLLM FP8 vs bf16 on an H100: 1.4x the throughput, and what it did to GSM8K accuracy</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:27:34 +0000</pubDate>
      <link>https://dev.to/runcrate/vllm-fp8-vs-bf16-on-an-h100-14x-the-throughput-and-what-it-did-to-gsm8k-accuracy-2n86</link>
      <guid>https://dev.to/runcrate/vllm-fp8-vs-bf16-on-an-h100-14x-the-throughput-and-what-it-did-to-gsm8k-accuracy-2n86</guid>
      <description>&lt;p&gt;vLLM can quantize a bf16 checkpoint to FP8 while it loads it: add &lt;code&gt;--quantization fp8&lt;/code&gt; and nothing else changes. On an H100 that should mean more tokens per second for the same money, but I wanted the actual numbers, and I wanted to know what it does to answer quality. So I rented one H100 on Runcrate today (2026-10-27), served the same 7B model in bf16 and then in FP8, ran the same load and the same 300 math questions against both, and terminated the machine as soon as the results were on disk.&lt;/p&gt;

&lt;p&gt;Short version: FP8 gave 1.40x the output throughput at 64 concurrent requests (6,147 against 4,401 tokens/s) and 1.55x the speed for a single user. On GSM8K it answered 270 of 300 questions right against 272 for bf16.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU:&lt;/strong&gt; one H100 PCIe (80 GB), US Midwest, $3.86 per hour on demand at launch time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Qwen/Qwen2.5-7B-Instruct, the regular bf16 checkpoint for both modes. FP8 here is vLLM's online quantization: weights are converted to FP8 at load time with a per-tensor scale, activations are scaled dynamically. The KV cache stays bf16 in both modes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server:&lt;/strong&gt; vLLM 0.11.0 (torch 2.8.0+cu128, transformers 4.57.6), &lt;code&gt;--dtype bfloat16 --max-model-len 4096 --gpu-memory-utilization 0.90&lt;/code&gt;, plus &lt;code&gt;--quantization fp8&lt;/code&gt; for the FP8 run. Fresh server per mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput:&lt;/strong&gt; &lt;code&gt;vllm bench serve&lt;/code&gt;, random prompts, 512 tokens in and exactly 256 out (&lt;code&gt;--ignore-eos&lt;/code&gt;), concurrency 1, 8, 32 and 64 (16, 64, 192 and 320 prompts), seed 42. Client on the same machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality:&lt;/strong&gt; the first 300 questions of the GSM8K test set, zero-shot, greedy (&lt;code&gt;temperature=0&lt;/code&gt;), up to 768 output tokens, final number parsed from a &lt;code&gt;#### &amp;lt;answer&amp;gt;&lt;/code&gt; line, 64 requests in flight.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;

&lt;p&gt;Launch (the &lt;code&gt;instance_type_id&lt;/code&gt; comes from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"fp8-h100","instance_type_id":"&amp;lt;id from /instances/types&amp;gt;",
       "ssh_key_id":"&amp;lt;your key id&amp;gt;","max_runtime_minutes":60}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 /opt/env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /opt/env/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;vllm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.11.0 &lt;span class="s1"&gt;'transformers&amp;lt;4.58'&lt;/span&gt; pandas pyarrow &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cu128

&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Qwen/Qwen2.5-7B-Instruct
&lt;span class="k"&gt;for &lt;/span&gt;MODE &lt;span class="k"&gt;in &lt;/span&gt;bf16 fp8&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;EXTRA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; fp8 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;EXTRA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--quantization fp8"&lt;/span&gt;
  vllm serve &lt;span class="nv"&gt;$M&lt;/span&gt; &lt;span class="nt"&gt;--dtype&lt;/span&gt; bfloat16 &lt;span class="nv"&gt;$EXTRA&lt;/span&gt; &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 4096 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.90 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &amp;amp;
  &lt;span class="k"&gt;until &lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; localhost:8000/v1/models &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  for &lt;/span&gt;C &lt;span class="k"&gt;in &lt;/span&gt;1 8 32 64&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    case&lt;/span&gt; &lt;span class="nv"&gt;$C&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;1&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16&lt;span class="p"&gt;;;&lt;/span&gt; 8&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;64&lt;span class="p"&gt;;;&lt;/span&gt; 32&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192&lt;span class="p"&gt;;;&lt;/span&gt; 64&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;320&lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
    vllm bench serve &lt;span class="nt"&gt;--backend&lt;/span&gt; vllm &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$M&lt;/span&gt; &lt;span class="nt"&gt;--dataset-name&lt;/span&gt; random &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--random-input-len&lt;/span&gt; 512 &lt;span class="nt"&gt;--random-output-len&lt;/span&gt; 256 &lt;span class="nt"&gt;--ignore-eos&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--num-prompts&lt;/span&gt; &lt;span class="nv"&gt;$N&lt;/span&gt; &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; &lt;span class="nv"&gt;$C&lt;/span&gt; &lt;span class="nt"&gt;--seed&lt;/span&gt; 42 &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--save-result&lt;/span&gt; &lt;span class="nt"&gt;--result-dir&lt;/span&gt; results &lt;span class="nt"&gt;--result-filename&lt;/span&gt; bench_&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;_c&lt;span class="nv"&gt;$C&lt;/span&gt;.json
  &lt;span class="k"&gt;done
  &lt;/span&gt;python gsm8k.py &lt;span class="nv"&gt;$M&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; results/gsm8k_&lt;span class="nv"&gt;$MODE&lt;/span&gt;.json   &lt;span class="c"&gt;# 300 questions, greedy, OpenAI client&lt;/span&gt;
  &lt;span class="nb"&gt;kill&lt;/span&gt; %1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;wait
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gsm8k.py&lt;/code&gt; is 40 lines: it loads &lt;code&gt;openai/gsm8k&lt;/code&gt; from the Hugging Face Hub, sends each question with the instruction "Solve this math word problem step by step. On the last line write only the final answer as '#### '.", and compares the last number with the reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Memory, as reported by vLLM at startup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Weights in GPU memory&lt;/th&gt;
&lt;th&gt;KV cache&lt;/th&gt;
&lt;th&gt;KV cache size&lt;/th&gt;
&lt;th&gt;Server ready after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;14.25 GiB&lt;/td&gt;
&lt;td&gt;51.27 GiB&lt;/td&gt;
&lt;td&gt;959,936 tokens&lt;/td&gt;
&lt;td&gt;66.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;8.14 GiB&lt;/td&gt;
&lt;td&gt;57.38 GiB&lt;/td&gt;
&lt;td&gt;1,074,320 tokens&lt;/td&gt;
&lt;td&gt;67.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Throughput and latency:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Output tok/s&lt;/th&gt;
&lt;th&gt;Per-user tok/s&lt;/th&gt;
&lt;th&gt;Median TTFT&lt;/th&gt;
&lt;th&gt;p99 TTFT&lt;/th&gt;
&lt;th&gt;$ per 1M output tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;26 ms&lt;/td&gt;
&lt;td&gt;28 ms&lt;/td&gt;
&lt;td&gt;10.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;td&gt;162&lt;/td&gt;
&lt;td&gt;21 ms&lt;/td&gt;
&lt;td&gt;25 ms&lt;/td&gt;
&lt;td&gt;6.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;766&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;119 ms&lt;/td&gt;
&lt;td&gt;141 ms&lt;/td&gt;
&lt;td&gt;1.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;1,173&lt;/td&gt;
&lt;td&gt;153&lt;/td&gt;
&lt;td&gt;88 ms&lt;/td&gt;
&lt;td&gt;109 ms&lt;/td&gt;
&lt;td&gt;0.91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;2,490&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;td&gt;163 ms&lt;/td&gt;
&lt;td&gt;502 ms&lt;/td&gt;
&lt;td&gt;0.43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;3,709&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;221 ms&lt;/td&gt;
&lt;td&gt;375 ms&lt;/td&gt;
&lt;td&gt;0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;4,401&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;td&gt;106 ms&lt;/td&gt;
&lt;td&gt;939 ms&lt;/td&gt;
&lt;td&gt;0.24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;6,147&lt;/td&gt;
&lt;td&gt;108&lt;/td&gt;
&lt;td&gt;98 ms&lt;/td&gt;
&lt;td&gt;755 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.17&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Per-user tok/s" is 1000 / median time per output token. Cost per 1M output tokens is &lt;code&gt;hourly price / (output tokens per second x 3600) x 1,000,000&lt;/code&gt;, counting generated tokens only.&lt;/p&gt;

&lt;p&gt;GSM8K, first 300 test questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Correct&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Wall time (64 in flight)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bf16&lt;/td&gt;
&lt;td&gt;272 / 300&lt;/td&gt;
&lt;td&gt;90.7%&lt;/td&gt;
&lt;td&gt;15.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP8&lt;/td&gt;
&lt;td&gt;270 / 300&lt;/td&gt;
&lt;td&gt;90.0%&lt;/td&gt;
&lt;td&gt;12.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two modes gave the same final answer on 272 of 300 questions. bf16 alone was right on 10 questions, FP8 alone on 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The biggest gain was for one user, not for the full batch.&lt;/strong&gt; At concurrency 1, FP8 decoded at 162 tokens/s against 105 (1.55x). Single-stream decoding reads every weight once per token, so halving the bytes of the weights goes almost straight into speed. At 64 users the gain was 1.40x, still large, but the batch is more compute bound there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Startup cost nothing extra.&lt;/strong&gt; The online conversion added about a second to a 67 s server start. There is no separate FP8 checkpoint to find or trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The quality cost was within noise on this test.&lt;/strong&gt; Two questions out of 300 is less than the disagreement between the modes in both directions (10 against 8). The final answer still differed on 28 questions, so FP8 does change outputs; it just did not make them worse on average here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The freed memory mostly went to KV cache, and this test did not need it.&lt;/strong&gt; About 6 GiB more cache (+12% tokens) matters with long contexts and many users; with 768-token requests and 64 users the cache was never full in either mode. The throughput gain above comes from faster matmuls and fewer bytes read, not from bigger batches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The p99 TTFT at concurrency 32 was better with FP8 but the median was worse&lt;/strong&gt; (221 ms against 163 ms). One run per setting; I would not read anything into that.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One model (7B), one GPU, one run per setting. Larger models, where weights take most of the memory, may gain more from FP8; smaller ones less.&lt;/li&gt;
&lt;li&gt;GSM8K with 300 questions has a standard error of about 1.7 points at 90% accuracy. It shows FP8 did not break math reasoning on this model; it cannot show a difference under about 3 points.&lt;/li&gt;
&lt;li&gt;This is online FP8 (per-tensor). Pre-quantized FP8 checkpoints with calibrated or per-channel scales, and FP8 KV cache (&lt;code&gt;--kv-cache-dtype fp8&lt;/code&gt;), are different settings and were not tested.&lt;/li&gt;
&lt;li&gt;Ada and Hopper GPUs (L40S, RTX 6000 Ada, H100, H200) have FP8 tensor cores. On older cards vLLM falls back to weight-only FP8 kernels, and the speed math changes.&lt;/li&gt;
&lt;li&gt;Price is the on-demand price at launch time and moves daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Pick an H100 from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt; and launch with the curl above, &lt;code&gt;max_runtime_minutes&lt;/code&gt; set as a safety net.&lt;/li&gt;
&lt;li&gt;Run the block above, copy &lt;code&gt;results/&lt;/code&gt; back, delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole run, including install, model download, both server starts and both benchmarks, was billed $0.58.&lt;/p&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=vllm-fp8-vs-bf16" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vllm</category>
      <category>llm</category>
      <category>gpu</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>vLLM prefix caching on vs off: 7.5x the throughput with a 4,000-token system prompt on one L40S</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:26:58 +0000</pubDate>
      <link>https://dev.to/runcrate/vllm-prefix-caching-on-vs-off-75x-the-throughput-with-a-4000-token-system-prompt-on-one-l40s-31ol</link>
      <guid>https://dev.to/runcrate/vllm-prefix-caching-on-vs-off-75x-the-throughput-with-a-4000-token-system-prompt-on-one-l40s-31ol</guid>
      <description>&lt;p&gt;Most chat traffic sends the same system prompt in front of every request. vLLM can keep the KV cache for that shared prefix and skip recomputing it (automatic prefix caching, on by default since the V1 engine). I wanted to know what that is worth in numbers, so I rented one L40S on Runcrate today (2026-10-27), ran the same chat-shaped load with prefix caching on and off, and terminated the machine as soon as the results were on disk.&lt;/p&gt;

&lt;p&gt;Short version: with a 4,000-token shared system prompt and 64 users at once, turning prefix caching off cut output throughput from 1,902 to 254 tokens per second and pushed p99 time to first token from 0.38 s to 23.9 s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPU:&lt;/strong&gt; one L40S (48 GB), Kansas City, US, $1.14 per hour on demand at launch time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Qwen/Qwen2.5-7B-Instruct, bf16, no quantization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server:&lt;/strong&gt; vLLM 0.11.0 (torch 2.8.0+cu128, transformers 4.57.6), &lt;code&gt;--max-model-len 8192 --gpu-memory-utilization 0.90&lt;/code&gt;. vLLM reported 14.25 GiB of weights and 24.26 GiB of KV cache (454,272 tokens).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two modes:&lt;/strong&gt; default (prefix caching on), and &lt;code&gt;--no-enable-prefix-caching&lt;/code&gt; (off). Same server flags otherwise, fresh server per mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load:&lt;/strong&gt; &lt;code&gt;vllm bench serve&lt;/code&gt; with the random dataset. Every request is one shared prefix (1,000 or 4,000 tokens, the "system prompt") followed by 200 unique tokens, and exactly 128 output tokens (&lt;code&gt;--ignore-eos&lt;/code&gt;). Concurrency 1, 16 and 64 (24, 160 and 384 requests). The client ran on the same machine, so no network is in the numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache hit rate&lt;/strong&gt; comes from vLLM's own &lt;code&gt;/metrics&lt;/code&gt; counters (&lt;code&gt;vllm:prefix_cache_hits&lt;/code&gt; / &lt;code&gt;vllm:prefix_cache_queries&lt;/code&gt;), read before and after every run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;

&lt;p&gt;Launch (the &lt;code&gt;instance_type_id&lt;/code&gt; comes from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"prefix-l40s","instance_type_id":"&amp;lt;id from /instances/types&amp;gt;",
       "ssh_key_id":"&amp;lt;your key id&amp;gt;","max_runtime_minutes":60}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 /opt/env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /opt/env/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;vllm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.11.0 &lt;span class="s1"&gt;'transformers&amp;lt;4.58'&lt;/span&gt; &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cu128

&lt;span class="nv"&gt;M&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Qwen/Qwen2.5-7B-Instruct
&lt;span class="k"&gt;for &lt;/span&gt;MODE &lt;span class="k"&gt;in &lt;/span&gt;on off&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;EXTRA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; off &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;EXTRA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--no-enable-prefix-caching"&lt;/span&gt;
  vllm serve &lt;span class="nv"&gt;$M&lt;/span&gt; &lt;span class="nt"&gt;--dtype&lt;/span&gt; bfloat16 &lt;span class="nv"&gt;$EXTRA&lt;/span&gt; &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 8192 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.90 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &amp;amp;
  &lt;span class="k"&gt;until &lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; localhost:8000/v1/models &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  for &lt;/span&gt;P &lt;span class="k"&gt;in &lt;/span&gt;1000 4000&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    for &lt;/span&gt;C &lt;span class="k"&gt;in &lt;/span&gt;1 16 64&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
      case&lt;/span&gt; &lt;span class="nv"&gt;$C&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;1&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24&lt;span class="p"&gt;;;&lt;/span&gt; 16&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;160&lt;span class="p"&gt;;;&lt;/span&gt; 64&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;384&lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
      vllm bench serve &lt;span class="nt"&gt;--backend&lt;/span&gt; vllm &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="nv"&gt;$M&lt;/span&gt; &lt;span class="nt"&gt;--dataset-name&lt;/span&gt; random &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--random-prefix-len&lt;/span&gt; &lt;span class="nv"&gt;$P&lt;/span&gt; &lt;span class="nt"&gt;--random-input-len&lt;/span&gt; 200 &lt;span class="nt"&gt;--random-output-len&lt;/span&gt; 128 &lt;span class="nt"&gt;--ignore-eos&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--num-prompts&lt;/span&gt; &lt;span class="nv"&gt;$N&lt;/span&gt; &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; &lt;span class="nv"&gt;$C&lt;/span&gt; &lt;span class="nt"&gt;--seed&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;P &lt;span class="o"&gt;+&lt;/span&gt; C&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--save-result&lt;/span&gt; &lt;span class="nt"&gt;--result-dir&lt;/span&gt; results &lt;span class="nt"&gt;--result-filename&lt;/span&gt; bench_&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;_p&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;P&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;_c&lt;span class="nv"&gt;$C&lt;/span&gt;.json
      curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8000/metrics | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^vllm:prefix_cache_(queries|hits)'&lt;/span&gt;
    &lt;span class="k"&gt;done
  done
  &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; %1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;wait
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;DELETE /api/v1/instances/&amp;lt;id&amp;gt;&lt;/code&gt; and a check that the instance list is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Cache&lt;/th&gt;
&lt;th&gt;Requests/s&lt;/th&gt;
&lt;th&gt;Output tok/s&lt;/th&gt;
&lt;th&gt;Median TTFT&lt;/th&gt;
&lt;th&gt;p99 TTFT&lt;/th&gt;
&lt;th&gt;Median time per token&lt;/th&gt;
&lt;th&gt;Hit rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;0.37&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;40 ms&lt;/td&gt;
&lt;td&gt;44 ms&lt;/td&gt;
&lt;td&gt;20.8 ms&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;0.36&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;103 ms&lt;/td&gt;
&lt;td&gt;106 ms&lt;/td&gt;
&lt;td&gt;20.8 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;5.27&lt;/td&gt;
&lt;td&gt;675&lt;/td&gt;
&lt;td&gt;163 ms&lt;/td&gt;
&lt;td&gt;208 ms&lt;/td&gt;
&lt;td&gt;22.5 ms&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;3.72&lt;/td&gt;
&lt;td&gt;476&lt;/td&gt;
&lt;td&gt;314 ms&lt;/td&gt;
&lt;td&gt;1,416 ms&lt;/td&gt;
&lt;td&gt;31.1 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;14.61&lt;/td&gt;
&lt;td&gt;1,870&lt;/td&gt;
&lt;td&gt;303 ms&lt;/td&gt;
&lt;td&gt;773 ms&lt;/td&gt;
&lt;td&gt;31.8 ms&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;6.63&lt;/td&gt;
&lt;td&gt;849&lt;/td&gt;
&lt;td&gt;507 ms&lt;/td&gt;
&lt;td&gt;5,940 ms&lt;/td&gt;
&lt;td&gt;71.5 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;0.37&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;57 ms&lt;/td&gt;
&lt;td&gt;63 ms&lt;/td&gt;
&lt;td&gt;21.1 ms&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;377 ms&lt;/td&gt;
&lt;td&gt;385 ms&lt;/td&gt;
&lt;td&gt;21.1 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;5.21&lt;/td&gt;
&lt;td&gt;667&lt;/td&gt;
&lt;td&gt;98 ms&lt;/td&gt;
&lt;td&gt;147 ms&lt;/td&gt;
&lt;td&gt;23.4 ms&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;1.90&lt;/td&gt;
&lt;td&gt;243&lt;/td&gt;
&lt;td&gt;850 ms&lt;/td&gt;
&lt;td&gt;4,987 ms&lt;/td&gt;
&lt;td&gt;59.1 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;on&lt;/td&gt;
&lt;td&gt;14.86&lt;/td&gt;
&lt;td&gt;1,902&lt;/td&gt;
&lt;td&gt;172 ms&lt;/td&gt;
&lt;td&gt;384 ms&lt;/td&gt;
&lt;td&gt;32.1 ms&lt;/td&gt;
&lt;td&gt;99.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;off&lt;/td&gt;
&lt;td&gt;1.99&lt;/td&gt;
&lt;td&gt;254&lt;/td&gt;
&lt;td&gt;1,936 ms&lt;/td&gt;
&lt;td&gt;23,935 ms&lt;/td&gt;
&lt;td&gt;240 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What that means for the bill, at $1.14 per hour, with the GPU kept busy at that concurrency:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;$ per 1,000 requests, cache on&lt;/th&gt;
&lt;th&gt;cache off&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.060&lt;/td&gt;
&lt;td&gt;0.085&lt;/td&gt;
&lt;td&gt;1.4x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;0.022&lt;/td&gt;
&lt;td&gt;0.048&lt;/td&gt;
&lt;td&gt;2.2x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.061&lt;/td&gt;
&lt;td&gt;0.167&lt;/td&gt;
&lt;td&gt;2.7x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4,000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;0.021&lt;/td&gt;
&lt;td&gt;0.160&lt;/td&gt;
&lt;td&gt;7.5x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;With the cache on, the length of the system prompt almost stopped mattering.&lt;/strong&gt; At 64 users, the 4,000-token prefix ran at 14.86 requests/s and the 1,000-token prefix at 14.61. The median time to first token was even lower with the longer prefix (172 ms against 303 ms), because a larger share of each prompt was already cached (99.6% hit rate against 88%). Without the cache, going from 1,000 to 4,000 tokens of prefix cut throughput by 3.3x.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost of "off" shows up in decode speed too, not only in TTFT.&lt;/strong&gt; Median time per output token at 64 users went from 32 ms to 240 ms with a 4,000-token prefix. Every new request has to prefill 4,200 tokens, and those prefills share the GPU with everyone else's decoding, so every user in the batch slows down, not only the one who just arrived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tail is where it hurts.&lt;/strong&gt; p99 time to first token at 64 users and a 4,000-token prefix was 23.9 s with the cache off. The median was 1.9 s. Some users wait for a long queue of prefills ahead of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For a single user, the cache saves latency but not money.&lt;/strong&gt; At concurrency 1 the GPU is bound by decoding one stream, so requests per second barely moved (0.37 against 0.33). The 4,000-token prefix still cost 377 ms of prefill per request without the cache and 57 ms with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first request pays.&lt;/strong&gt; The 85% hit rate at 1,000 tokens and concurrency 1 is close to the share of each prompt that is shared (1,000 of 1,200 tokens). The cache never helps the unique part of a prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to take from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prefix caching is already on in current vLLM. These "off" numbers are also what you get &lt;strong&gt;with&lt;/strong&gt; the cache on if your prompts do not actually share a prefix: a timestamp, user name or request id at the top of the system prompt changes the first tokens and every request misses. Put the parts that never change first and the parts that change last.&lt;/li&gt;
&lt;li&gt;If your traffic is many users behind one long system prompt, size the GPU from the cache-on numbers, but check the hit rate in &lt;code&gt;/metrics&lt;/code&gt; in production to see whether you really get them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One run per setting, on one machine, on one day. Treat differences under about 10% as noise.&lt;/li&gt;
&lt;li&gt;Synthetic prompts from vLLM's random dataset: one prefix shared by every request. Real traffic mixes several system prompts, multi-turn history and different lengths; hit rates will be lower.&lt;/li&gt;
&lt;li&gt;The cache could hold the whole prefix easily here (454k tokens of KV cache). With many distinct long prefixes and a full cache, evictions would lower the hit rate.&lt;/li&gt;
&lt;li&gt;Price is the on-demand price at launch time and moves daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Pick an L40S from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt; and launch with the curl above, &lt;code&gt;max_runtime_minutes&lt;/code&gt; set as a safety net.&lt;/li&gt;
&lt;li&gt;Run the block above, copy &lt;code&gt;results/&lt;/code&gt; back, delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole run, including install, model download and both server starts, was billed $0.38.&lt;/p&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=vllm-prefix-caching" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vllm</category>
      <category>llm</category>
      <category>gpu</category>
      <category>performance</category>
    </item>
    <item>
      <title>4 GPUs, 2x speed: why PyTorch DDP stalled on a PCIe-only 4x RTX A6000, and two flags that took it to 3.6x</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:22:37 +0000</pubDate>
      <link>https://dev.to/runcrate/4-gpus-2x-speed-why-pytorch-ddp-stalled-on-a-pcie-only-4x-rtx-a6000-and-two-flags-that-took-it-1ll9</link>
      <guid>https://dev.to/runcrate/4-gpus-2x-speed-why-pytorch-ddp-stalled-on-a-pcie-only-4x-rtx-a6000-and-two-flags-that-took-it-1ll9</guid>
      <description>&lt;p&gt;Four GPUs should train about four times faster than one. On a rented 4x RTX A6000 machine this morning (2026-10-26), plain PyTorch DDP gave me 2x. This is the notebook of finding out why, and the two flags that took it to 3.6x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Machine:&lt;/strong&gt; one Runcrate 4x RTX A6000 (48 GB each) VM, US Midwest, $2.64/hour for the whole machine at launch time. Two separate machines of the same type over the morning: the first for the baseline, the second for the fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topology&lt;/strong&gt; (&lt;code&gt;nvidia-smi topo -m&lt;/code&gt;): every GPU pair is &lt;code&gt;PHB&lt;/code&gt;, i.e. GPU to GPU traffic crosses the CPU's PCIe host bridge. No NVLink. PCIe gen 3 x16 per card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; a GPT-2 style decoder written in plain PyTorch, two sizes: 124M parameters (12 layers, 768 wide) and 355M (24 layers, 1024 wide). Random tokens, sequence 1024, micro-batch 8 per GPU, bf16 autocast, fused AdamW.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software:&lt;/strong&gt; torch 2.8.0+cu128 (first machine) and 2.11.0+cu128 (second), NCCL as shipped in the wheel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metric:&lt;/strong&gt; tokens/s from the median step time over 40 steps after 10 warm-up steps. Weak scaling: each GPU always gets its own micro-batch of 8.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# baseline: 1, 2 and 4 processes on the same machine&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;N &lt;span class="k"&gt;in &lt;/span&gt;1 2 4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;torchrun &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt; &lt;span class="nv"&gt;$N&lt;/span&gt; train_ddp.py &lt;span class="nt"&gt;--size&lt;/span&gt; small&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# all-reduce bandwidth, nccl-tests style (bus bandwidth = algbw x 2(n-1)/n)&lt;/span&gt;
torchrun &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt; 4 allreduce.py

&lt;span class="c"&gt;# the fixes&lt;/span&gt;
torchrun &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt; 4 train_ddp.py &lt;span class="nt"&gt;--size&lt;/span&gt; small &lt;span class="nt"&gt;--hook&lt;/span&gt; bf16            &lt;span class="c"&gt;# bf16 gradient compression&lt;/span&gt;
torchrun &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt; 4 train_ddp.py &lt;span class="nt"&gt;--size&lt;/span&gt; small &lt;span class="nt"&gt;--accum&lt;/span&gt; 4              &lt;span class="c"&gt;# all-reduce every 4 micro-batches&lt;/span&gt;
torchrun &lt;span class="nt"&gt;--standalone&lt;/span&gt; &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt; 4 train_ddp.py &lt;span class="nt"&gt;--size&lt;/span&gt; small &lt;span class="nt"&gt;--accum&lt;/span&gt; 4 &lt;span class="nt"&gt;--hook&lt;/span&gt; bf16  &lt;span class="c"&gt;# both&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two fixes are a few lines each in the training loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;torch.distributed.algorithms.ddp_comm_hooks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;default_hooks&lt;/span&gt;
&lt;span class="n"&gt;ddp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DDP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;local_rank&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;gradient_as_bucket_view&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ddp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_comm_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default_hooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bf16_compress_hook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# send gradients as bf16
&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                                            &lt;span class="c1"&gt;# gradient accumulation
&lt;/span&gt;    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ddp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_sync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;accum&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nullcontext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autocast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bfloat16&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ddp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;next_batch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;accum&lt;/span&gt;
        &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set_to_none&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Baseline: 2x from 4 GPUs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;GPUs&lt;/th&gt;
&lt;th&gt;Tokens/s&lt;/th&gt;
&lt;th&gt;Speed-up vs 1 GPU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;124M&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;61,907&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;124M&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;77,287&lt;/td&gt;
&lt;td&gt;1.25x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;124M&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;127,682&lt;/td&gt;
&lt;td&gt;2.06x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;355M&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;25,762&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;355M&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;30,431&lt;/td&gt;
&lt;td&gt;1.18x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;355M&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;48,611&lt;/td&gt;
&lt;td&gt;1.89x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The all-reduce explains it. NCCL bus bandwidth between the four cards was &lt;strong&gt;4.3 GB/s&lt;/strong&gt; (2 GPUs: 3.9 GB/s), flat from 8 MB to 1 GB messages. DDP all-reduces fp32 gradients: about 500 MB per step for the 124M model. At 2.9 GB/s of algorithm bandwidth that is roughly 170 ms of communication against a 130 ms compute step, and 4 GPUs measured about 255 ms per step. The GPUs spend more time talking than computing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes, on 4 GPUs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Tokens/s&lt;/th&gt;
&lt;th&gt;Speed-up vs 1 GPU (same variant)&lt;/th&gt;
&lt;th&gt;$ per 1B training tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;124M&lt;/td&gt;
&lt;td&gt;1 GPU&lt;/td&gt;
&lt;td&gt;64,912&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;td&gt;2.82 (one GPU's share, $0.66/h)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;124M&lt;/td&gt;
&lt;td&gt;4 GPUs, plain DDP&lt;/td&gt;
&lt;td&gt;129,032&lt;/td&gt;
&lt;td&gt;1.99x&lt;/td&gt;
&lt;td&gt;5.68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;124M&lt;/td&gt;
&lt;td&gt;4 GPUs, bf16 gradients&lt;/td&gt;
&lt;td&gt;172,144&lt;/td&gt;
&lt;td&gt;2.65x&lt;/td&gt;
&lt;td&gt;4.26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;124M&lt;/td&gt;
&lt;td&gt;4 GPUs, accumulate 4&lt;/td&gt;
&lt;td&gt;211,834&lt;/td&gt;
&lt;td&gt;3.20x&lt;/td&gt;
&lt;td&gt;3.46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;124M&lt;/td&gt;
&lt;td&gt;4 GPUs, both&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;234,175&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.54x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;355M&lt;/td&gt;
&lt;td&gt;1 GPU&lt;/td&gt;
&lt;td&gt;27,109&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;td&gt;6.76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;355M&lt;/td&gt;
&lt;td&gt;4 GPUs, plain DDP&lt;/td&gt;
&lt;td&gt;51,711&lt;/td&gt;
&lt;td&gt;1.91x&lt;/td&gt;
&lt;td&gt;14.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;355M&lt;/td&gt;
&lt;td&gt;4 GPUs, bf16 gradients&lt;/td&gt;
&lt;td&gt;73,653&lt;/td&gt;
&lt;td&gt;2.72x&lt;/td&gt;
&lt;td&gt;9.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;355M&lt;/td&gt;
&lt;td&gt;4 GPUs, accumulate 4&lt;/td&gt;
&lt;td&gt;86,507&lt;/td&gt;
&lt;td&gt;3.10x&lt;/td&gt;
&lt;td&gt;8.48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;355M&lt;/td&gt;
&lt;td&gt;4 GPUs, both&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99,152&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.66x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(1-GPU tokens/s with accumulation 4: 66,189 and 27,893, so the "both" rows are 88% and 89% scaling efficiency against the same recipe on one GPU.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The interconnect, not the GPU, set the speed.&lt;/strong&gt; 4.3 GB/s of all-reduce bandwidth is a fraction of what PCIe gen 3 x16 can carry. With every pair going through the host bridge, plain DDP on four cards only doubled throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forcing peer-to-peer made it worse.&lt;/strong&gt; &lt;code&gt;NCCL_P2P_LEVEL=SYS&lt;/code&gt; (use P2P even across the host bridge) halved the all-reduce bandwidth to 2.3 GB/s and dropped 124M training from 129k to 80k tokens/s. NCCL's default choice was right; don't override it on a PHB machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two cheap changes recovered most of it.&lt;/strong&gt; bf16 gradient compression halves the bytes; accumulating 4 micro-batches per all-reduce cuts the number of all-reduces by 4. Together: 3.5x to 3.7x on four GPUs. Accumulation changes your effective batch size (32 per GPU here), so retune the learning rate if you adopt it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per token, one GPU still won.&lt;/strong&gt; Even with both fixes, a token cost about 10% more on four GPUs than on one, because four GPUs at 88% efficiency is still four GPUs. Rent the 4x machine when wall-clock time matters (a 3.5x shorter run), not to save money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bigger model scaled slightly better,&lt;/strong&gt; as expected: more compute per byte of gradient.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Random tokens and a synthetic model: this measures throughput, not convergence. bf16 gradient compression slightly changes numerics.&lt;/li&gt;
&lt;li&gt;Two machines of the same type, one run per configuration. Topology varies between machines; run &lt;code&gt;nvidia-smi topo -m&lt;/code&gt; and a 30-second all-reduce test before a long job.&lt;/li&gt;
&lt;li&gt;Machines with NVLink behave completely differently; this is a PCIe-only result.&lt;/li&gt;
&lt;li&gt;Prices are on-demand at launch time on 2026-10-26.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Launch a 4x GPU instance through &lt;code&gt;POST /api/v1/instances&lt;/code&gt; with &lt;code&gt;max_runtime_minutes&lt;/code&gt; set.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uv venv /opt/env &amp;amp;&amp;amp; . /opt/env/bin/activate &amp;amp;&amp;amp; uv pip install torch numpy --torch-backend=cu128&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Copy &lt;code&gt;train_ddp.py&lt;/code&gt; and &lt;code&gt;allreduce.py&lt;/code&gt; (about 100 lines together; the core is the snippet above) and run the torchrun commands.&lt;/li&gt;
&lt;li&gt;Delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both 4x machines together, including setup and every configuration above, were billed $0.92.&lt;/p&gt;

&lt;p&gt;If you want to try it on your own model: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=ddp-scaling" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the runs described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pytorch</category>
      <category>gpu</category>
      <category>machinelearning</category>
      <category>performance</category>
    </item>
    <item>
      <title>How long until a rented GPU runs CUDA? Launch to first matmul on six GPU types, timed</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:19:54 +0000</pubDate>
      <link>https://dev.to/runcrate/how-long-until-a-rented-gpu-runs-cuda-launch-to-first-matmul-on-six-gpu-types-timed-3oif</link>
      <guid>https://dev.to/runcrate/how-long-until-a-rented-gpu-runs-cuda-launch-to-first-matmul-on-six-gpu-types-timed-3oif</guid>
      <description>&lt;p&gt;"How long until my GPU is ready?" is the question I care about most when I rent one for a short job, and it is the number nobody publishes. So this morning (2026-10-26) I launched single-GPU machines of six types on Runcrate through the API, one after another in one batch, and timed every step until a real CUDA matmul finished. Then I deleted each machine the second it was done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;For each machine, seconds since the create call returned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API running:&lt;/strong&gt; the instance API reports &lt;code&gt;running&lt;/code&gt; and hands out the IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First SSH:&lt;/strong&gt; my script's first successful &lt;code&gt;ssh root@ip echo ok&lt;/code&gt; (it retries every 2 s once it has the IP).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First CUDA op:&lt;/strong&gt; after SSH, install &lt;code&gt;uv&lt;/code&gt;, create a venv, &lt;code&gt;uv pip install torch --torch-backend=cu128&lt;/code&gt;, then multiply two 4096x4096 matrices on the GPU and synchronize.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also read two clocks on the machine itself, because they tell you where the time goes: kernel boot (&lt;code&gt;uptime -s&lt;/code&gt;) and when &lt;code&gt;ssh.service&lt;/code&gt; came up (&lt;code&gt;systemctl show ssh -p ActiveEnterTimestamp&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. launch (instance_type_id from GET /api/v1/instances/types)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"cold-l40s","instance_type_id":"&amp;lt;id&amp;gt;","ssh_key_id":"&amp;lt;key id&amp;gt;","max_runtime_minutes":30}'&lt;/span&gt;

&lt;span class="c"&gt;# 2. poll every 10 s until status is running and ip is set&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances/&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 3. on the machine&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
uv venv &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 /opt/env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /opt/env/bin/activate
uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; torch &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cu128
python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import torch,time;t=time.time();x=torch.randn(4096,4096,device='cuda');y=x@x;torch.cuda.synchronize();print(time.time()-t)"&lt;/span&gt;

&lt;span class="c"&gt;# 4. delete&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE https://www.runcrate.ai/api/v1/instances/&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;All VMs, Ubuntu image, one GPU each, launched within 4 seconds of each other at 09:02 UTC.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runcrate GPU&lt;/th&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;th&gt;API running&lt;/th&gt;
&lt;th&gt;First SSH&lt;/th&gt;
&lt;th&gt;First CUDA op&lt;/th&gt;
&lt;th&gt;Billed for the whole run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;Des Moines, US&lt;/td&gt;
&lt;td&gt;1.14&lt;/td&gt;
&lt;td&gt;151 s&lt;/td&gt;
&lt;td&gt;164 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;186 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.010&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A100 80GB&lt;/td&gt;
&lt;td&gt;Montreal, CA&lt;/td&gt;
&lt;td&gt;1.73&lt;/td&gt;
&lt;td&gt;303 s&lt;/td&gt;
&lt;td&gt;319 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;337 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.016&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A4000&lt;/td&gt;
&lt;td&gt;New York, US&lt;/td&gt;
&lt;td&gt;1.04&lt;/td&gt;
&lt;td&gt;452 s&lt;/td&gt;
&lt;td&gt;463 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;482 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.008&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V100 16GB&lt;/td&gt;
&lt;td&gt;New York, US&lt;/td&gt;
&lt;td&gt;0.36&lt;/td&gt;
&lt;td&gt;611 s&lt;/td&gt;
&lt;td&gt;625 s&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;673 s&lt;/strong&gt; (see below)&lt;/td&gt;
&lt;td&gt;$0.006&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 6000 Ada&lt;/td&gt;
&lt;td&gt;US Midwest&lt;/td&gt;
&lt;td&gt;1.08&lt;/td&gt;
&lt;td&gt;620 s&lt;/td&gt;
&lt;td&gt;635 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;670 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.014&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;US Midwest&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;629 s&lt;/td&gt;
&lt;td&gt;647 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;707 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.014&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And from the machines' own clocks, same runs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runcrate GPU&lt;/th&gt;
&lt;th&gt;Kernel booted&lt;/th&gt;
&lt;th&gt;ssh.service up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;65 s&lt;/td&gt;
&lt;td&gt;149 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A100 80GB&lt;/td&gt;
&lt;td&gt;116 s&lt;/td&gt;
&lt;td&gt;293 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A4000&lt;/td&gt;
&lt;td&gt;143 s&lt;/td&gt;
&lt;td&gt;455 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V100 16GB&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;td&gt;61 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 6000 Ada&lt;/td&gt;
&lt;td&gt;76 s&lt;/td&gt;
&lt;td&gt;97 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;95 s&lt;/td&gt;
&lt;td&gt;121 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The torch step on its own (venv + wheel install + first matmul): 10 to 48 s for the install, 0.2 to 1.0 s for the first matmul, which is mostly CUDA context creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The spread is 4x, and it is not about the GPU.&lt;/strong&gt; The L40S was doing math in about 3 minutes, the A6000 in almost 12. Same API, same image, same script. Cold start depends on where the machine comes from far more than on which card is in it, so if you launch often, measure the specific region you use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On three types, the machine was ready long before the API said so.&lt;/strong&gt; The V100, RTX 6000 Ada and A6000 booted and had sshd listening in 1 to 2 minutes, but the API reported them running about 8 minutes later. Nothing in my script could have used them earlier, because the IP arrives with &lt;code&gt;running&lt;/code&gt;. The upside: billing starts at &lt;code&gt;running&lt;/code&gt;, which is why the bills in the table are a cent or two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The newest PyTorch wheels no longer run on a V100.&lt;/strong&gt; &lt;code&gt;uv pip install torch --torch-backend=cu128&lt;/code&gt; resolved to torch 2.11.0, which warns that it was built for compute capability 7.5 and up, and the matmul then failed with &lt;code&gt;cudaErrorNoKernelImageForDevice&lt;/code&gt;. The V100 is 7.0. On an earlier V100 run the same command had resolved to torch 2.8.0, which worked. If you rent Volta cards, pin torch (&lt;code&gt;torch==2.8.*&lt;/code&gt; worked here) instead of taking the latest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every create call works.&lt;/strong&gt; Over the day I made 18 create calls. Two (an RTX 4090 and an RTX 5090 VM in the same region) came back within 7 seconds with &lt;code&gt;500 provider_unavailable&lt;/code&gt;, and one type was gone between listing it and launching it (&lt;code&gt;400 ... no longer available&lt;/code&gt;). Both failures are fast and cost nothing, so the right client behaviour is: read &lt;code&gt;/instances/types&lt;/code&gt; right before launching, and fall back to the next option on a 4xx/5xx.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A container is a different animal.&lt;/strong&gt; In an earlier batch I launched an RTX 5090 as a container instead of a VM: its clock said it started 4 seconds after the create call. If startup time matters more than a full VM, check the &lt;code&gt;deployment_type&lt;/code&gt; column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own polling made my first batch useless.&lt;/strong&gt; I first polled nine machines every 2 seconds, hit the API's rate limit (HTTP 429) for several minutes, and the "running" times from that batch were wrong by minutes. This table is from a second batch polling every 10 seconds. Poll slowly.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One launch per GPU type in this table, at one time of day. Cold start depends on what is in stock at that moment; treat it as a sample, not a promise.&lt;/li&gt;
&lt;li&gt;"First CUDA op" includes downloading a 900 MB torch wheel. A pre-built image or a volume with a cached venv removes most of the 10 to 48 s.&lt;/li&gt;
&lt;li&gt;The prices are the on-demand prices at launch time and move daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key, add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;For each type you care about: launch with the curl above, poll every 10 s or slower, SSH in as soon as there is an IP, run the torch block, delete.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;uptime -s&lt;/code&gt; and &lt;code&gt;systemctl show ssh -p ActiveEnterTimestamp&lt;/code&gt; on the machine if you want to know where the time went.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The six machines in the table were billed $0.07 in total.&lt;/p&gt;

&lt;p&gt;If you want to time it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=gpu-cold-start" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the runs described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>cuda</category>
      <category>pytorch</category>
      <category>devops</category>
    </item>
    <item>
      <title>vLLM on three GPUs: Qwen2.5-7B tokens per second and cost per 1M tokens on RTX A6000, L40S and H100</title>
      <dc:creator>Runcrate</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:05:51 +0000</pubDate>
      <link>https://dev.to/runcrate/vllm-on-three-gpus-qwen25-7b-tokens-per-second-and-cost-per-1m-tokens-on-rtx-a6000-l40s-and-h100-38bn</link>
      <guid>https://dev.to/runcrate/vllm-on-three-gpus-qwen25-7b-tokens-per-second-and-cost-per-1m-tokens-on-rtx-a6000-l40s-and-h100-38bn</guid>
      <description>&lt;p&gt;I wanted one number before picking a GPU for a small self-hosted model: what does a million generated tokens cost on each card, once the server is actually busy? Spec sheets don't answer that, so I rented three single-GPU machines on Runcrate this morning (2026-10-26), ran the same vLLM benchmark on each, and terminated them as soon as the numbers were on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Qwen/Qwen2.5-7B-Instruct, bf16, no quantization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server:&lt;/strong&gt; vLLM 0.11.0 (torch 2.8.0+cu128, transformers 4.57.6), &lt;code&gt;--max-model-len 4096 --gpu-memory-utilization 0.90&lt;/code&gt;, everything else default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load:&lt;/strong&gt; &lt;code&gt;vllm bench serve&lt;/code&gt; with random prompts, 512 tokens in and exactly 256 tokens out (&lt;code&gt;--ignore-eos&lt;/code&gt;), at concurrency 1, 8, 32 and 64 (16, 64, 192 and 320 prompts), seed 42. The client ran on the same machine as the server, so there is no network in these numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machines&lt;/strong&gt; (on-demand price at launch time):&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runcrate GPU&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;$/hour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;48 GB&lt;/td&gt;
&lt;td&gt;Des Moines, US&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;48 GB&lt;/td&gt;
&lt;td&gt;Kansas City, US&lt;/td&gt;
&lt;td&gt;1.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;80 GB&lt;/td&gt;
&lt;td&gt;Montreal, CA&lt;/td&gt;
&lt;td&gt;3.55&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cost per 1M output tokens below is simply &lt;code&gt;hourly price / (output tokens per second x 3600) x 1,000,000&lt;/code&gt;. It counts only generated tokens; the 512 prompt tokens per request are free riders in this math.&lt;/p&gt;

&lt;h2&gt;
  
  
  The commands
&lt;/h2&gt;

&lt;p&gt;Launch (one call per GPU; the &lt;code&gt;instance_type_id&lt;/code&gt; comes from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.runcrate.ai/api/v1/instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RUNCRATE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"vllm-a6000","instance_type_id":"&amp;lt;id from /instances/types&amp;gt;",
       "ssh_key_id":"&amp;lt;your key id&amp;gt;","max_runtime_minutes":45}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;max_runtime_minutes&lt;/code&gt; is the safety net: the machine stops itself even if my script dies. Then, on the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;
uv venv &lt;span class="nt"&gt;--python&lt;/span&gt; 3.12 /opt/env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; /opt/env/bin/activate
uv pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;vllm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.11.0 &lt;span class="s1"&gt;'transformers&amp;lt;4.58'&lt;/span&gt; &lt;span class="nt"&gt;--torch-backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cu128

vllm serve Qwen/Qwen2.5-7B-Instruct &lt;span class="nt"&gt;--dtype&lt;/span&gt; bfloat16 &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 4096 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpu-memory-utilization&lt;/span&gt; 0.90 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &amp;amp;

&lt;span class="k"&gt;for &lt;/span&gt;C &lt;span class="k"&gt;in &lt;/span&gt;1 8 32 64&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  case&lt;/span&gt; &lt;span class="nv"&gt;$C&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;1&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16&lt;span class="p"&gt;;;&lt;/span&gt; 8&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;64&lt;span class="p"&gt;;;&lt;/span&gt; 32&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192&lt;span class="p"&gt;;;&lt;/span&gt; 64&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;N&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;320&lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
  vllm bench serve &lt;span class="nt"&gt;--backend&lt;/span&gt; vllm &lt;span class="nt"&gt;--model&lt;/span&gt; Qwen/Qwen2.5-7B-Instruct &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dataset-name&lt;/span&gt; random &lt;span class="nt"&gt;--random-input-len&lt;/span&gt; 512 &lt;span class="nt"&gt;--random-output-len&lt;/span&gt; 256 &lt;span class="nt"&gt;--ignore-eos&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--num-prompts&lt;/span&gt; &lt;span class="nv"&gt;$N&lt;/span&gt; &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; &lt;span class="nv"&gt;$C&lt;/span&gt; &lt;span class="nt"&gt;--seed&lt;/span&gt; 42 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--save-result&lt;/span&gt; &lt;span class="nt"&gt;--result-dir&lt;/span&gt; results &lt;span class="nt"&gt;--result-filename&lt;/span&gt; bench_c&lt;span class="nv"&gt;$C&lt;/span&gt;.json
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;DELETE /api/v1/instances/&amp;lt;id&amp;gt;&lt;/code&gt; and a check that the list is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;Output tok/s&lt;/th&gt;
&lt;th&gt;Per-user tok/s&lt;/th&gt;
&lt;th&gt;Median TTFT&lt;/th&gt;
&lt;th&gt;p99 TTFT&lt;/th&gt;
&lt;th&gt;$ per 1M output tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;82 ms&lt;/td&gt;
&lt;td&gt;85 ms&lt;/td&gt;
&lt;td&gt;4.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;307&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;289 ms&lt;/td&gt;
&lt;td&gt;529 ms&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;1,012&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;479 ms&lt;/td&gt;
&lt;td&gt;1,940 ms&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;1,727&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;259 ms&lt;/td&gt;
&lt;td&gt;3,347 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.12&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;48 ms&lt;/td&gt;
&lt;td&gt;49 ms&lt;/td&gt;
&lt;td&gt;6.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;354&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;161 ms&lt;/td&gt;
&lt;td&gt;299 ms&lt;/td&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;1,082&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;279 ms&lt;/td&gt;
&lt;td&gt;1,050 ms&lt;/td&gt;
&lt;td&gt;0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;1,887&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;146 ms&lt;/td&gt;
&lt;td&gt;2,169 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.17&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;td&gt;25 ms&lt;/td&gt;
&lt;td&gt;26 ms&lt;/td&gt;
&lt;td&gt;9.47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;776&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;62 ms&lt;/td&gt;
&lt;td&gt;125 ms&lt;/td&gt;
&lt;td&gt;1.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;2,566&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;233 ms&lt;/td&gt;
&lt;td&gt;410 ms&lt;/td&gt;
&lt;td&gt;0.38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;4,515&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;101 ms&lt;/td&gt;
&lt;td&gt;960 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.22&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Per-user tok/s" is 1000 / median time per output token, i.e. how fast one stream feels to the person reading it.&lt;/p&gt;

&lt;p&gt;How long each machine took before it could serve anything (measured on the machine):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;vLLM install&lt;/th&gt;
&lt;th&gt;Model download (15 GB)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;vllm serve&lt;/code&gt; until ready&lt;/th&gt;
&lt;th&gt;First chat reply&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX A6000&lt;/td&gt;
&lt;td&gt;29 s&lt;/td&gt;
&lt;td&gt;103 s&lt;/td&gt;
&lt;td&gt;114 s&lt;/td&gt;
&lt;td&gt;1.22 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L40S&lt;/td&gt;
&lt;td&gt;7 s&lt;/td&gt;
&lt;td&gt;78 s&lt;/td&gt;
&lt;td&gt;67 s&lt;/td&gt;
&lt;td&gt;0.85 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 PCIe&lt;/td&gt;
&lt;td&gt;4 s&lt;/td&gt;
&lt;td&gt;13 s&lt;/td&gt;
&lt;td&gt;54 s&lt;/td&gt;
&lt;td&gt;0.56 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The L40S is barely faster than the A6000 at decoding.&lt;/strong&gt; It has roughly twice the bf16 compute on paper, but single-stream decode on a 7B model is limited by memory bandwidth, and the two cards are close there. 48 vs 42 tokens/s at concurrency 1, 1,887 vs 1,727 at 64. Per dollar, the A6000 wins at every load level I tried.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The H100 is not the per-token deal, it is the latency deal.&lt;/strong&gt; It produced 2.6x the A6000's throughput at concurrency 64 for 4.8x the hourly price. But every user still saw 79 tokens/s at that load, against 32 on the other two cards, and the p99 time to first token stayed under a second. If your product is a chat UI with real people waiting, that is what you are paying for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At concurrency 1, every GPU is expensive.&lt;/strong&gt; $4.86 to $9.47 per million tokens. Batching is where the money is: going from 1 to 64 concurrent requests cut the cost per token by about 40x on all three cards. If your traffic can't keep the batch full, a smaller, older card idles more cheaply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p99 TTFT at concurrency 64 on the A6000 was 3.3 s,&lt;/strong&gt; while the median was 259 ms. Some requests wait for a whole batch slot. Look at tail latency before you take the cost-per-token number at face value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup time varied more than benchmark time.&lt;/strong&gt; The 15 GB model download took 13 s on one machine and 103 s on another. For short experiments, that download is a real part of the bill; put the weights on a volume if you will start the server more than once.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One run per GPU, on one morning. Treat differences under about 10% as noise.&lt;/li&gt;
&lt;li&gt;Random prompts, fixed 512/256 shape. Real chat traffic has prefix reuse and varied lengths; prefix caching would help all three cards.&lt;/li&gt;
&lt;li&gt;These are PCIe cards, one per machine. No tensor parallelism, no FP8, no speculative decoding. An FP8 checkpoint would likely change the H100 math more than the others.&lt;/li&gt;
&lt;li&gt;Prices are the on-demand prices at launch time on 2026-10-26 and move daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create an API key and add an SSH key (&lt;code&gt;POST /api/v1/ssh-keys&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Pick an instance type from &lt;code&gt;GET /api/v1/instances/types&lt;/code&gt; and launch with the curl above, &lt;code&gt;max_runtime_minutes&lt;/code&gt; set.&lt;/li&gt;
&lt;li&gt;Run the setup and benchmark block, copy &lt;code&gt;results/&lt;/code&gt; back, delete the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The three machines, including setup time, were billed $1.07 together ($0.15 for the A6000, $0.29 for the L40S, $0.64 for the H100).&lt;/p&gt;

&lt;p&gt;If you want to run it yourself: &lt;a href="https://www.runcrate.ai/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=vllm-tokens-per-dollar" rel="noopener noreferrer"&gt;runcrate.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the Runcrate team, with AI assistance. The numbers come from the run described above; raw logs are kept with the post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vllm</category>
      <category>llm</category>
      <category>gpu</category>
      <category>benchmark</category>
    </item>
  </channel>
</rss>
