<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about cublas)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/cublas.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Original site content © 2022–2026 Tiny Machines Workshop, LLC, except where otherwise noted. Some rights reserved.</copyright><lastBuildDate>Sat, 31 Oct 2026 21:32:01 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Why FP16 Is Slow on a Tesla P40, and Why FP32 Was Slow Too</title><link>https://tinycomputers.io/posts/why-fp16-is-slow-on-a-tesla-p40.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/why-fp16-is-slow-on-a-tesla-p40_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;22 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;In February I ran &lt;a href="https://tinycomputers.io/posts/image-editing-on-10-year-old-gpus-nvidia-p40-vs-amd-strix-halo.html"&gt;FireRed-Image-Edit on four Tesla P40s&lt;/a&gt; and found that FP32 was faster than FP16, about 9 minutes per denoising step in FP16 against 1.5 to 2.4 minutes in FP32. I explained it with one sentence: FP16 on Pascal "is handled through slower pathways." That sentence names no pathway. It was a guess dressed up as an explanation.&lt;/p&gt;
&lt;p&gt;Then Sevan emailed me a second data point. Sevan runs the AI behind a free wardrobe app on a single P40 and had been loading a virtual try-on model, Leffa, in FP16 since launch to save memory. Switching to FP32 took a 20-step render from 1,241 seconds to 85: 63.8 seconds per step in FP16 against 4.7 in FP32, with images that differ by 0.77/255 per pixel on average. The measurements and the dtype rule Sevan landed on are in &lt;a href="https://fring.sevan.zone/en/guides/tesla-p40-fp16-vs-fp32" rel="nofollow"&gt;Sevan's write-up&lt;/a&gt;. Thank you, Sevan, for taking the time to write to me; it is the reason this post exists.&lt;/p&gt;
&lt;p&gt;Two independent P40s, two different diffusion models, the same 4x to 14x penalty for FP16. I wanted to know which operation causes it, so I measured each one.&lt;/p&gt;
&lt;h3&gt;The setup&lt;/h3&gt;
&lt;p&gt;Everything ran on the same machine as the February post: four Tesla P40s in a Penguin Computing 2U chassis, NVIDIA driver 580.178.04, and the same Python environment, PyTorch 2.6.0 with CUDA 12.4 and cuDNN 9.1. Single-card tests ran on one P40, which held its 1,531 MHz boost clock throughout (the shop is cold enough that the cards never passed 33°C). I used FireRed-Image-Edit-1.0's real weights, sized every test to the February edit job (a roughly 1024x1024 target plus a 1024x1024 reference image, which comes to 8,192 image tokens plus a short prompt), and ran each precision in its own process.&lt;/p&gt;
&lt;p&gt;There are three layers of test: raw operations (matrix multiply, attention, convolution, elementwise math), the first six of FireRed's 60 transformer blocks with real weights, and the full pipeline in FP32 across all four cards.&lt;/p&gt;
&lt;h3&gt;FP16 matrix math is not the problem&lt;/h3&gt;
&lt;p&gt;The P40's GP102 chip does FP16 arithmetic at 1/64 of its FP32 rate, so the natural assumption is that every FP16 operation crawls. Matrix multiplies do not:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation (one P40)&lt;/th&gt;
&lt;th&gt;FP32&lt;/th&gt;
&lt;th&gt;FP16&lt;/th&gt;
&lt;th&gt;BF16&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matrix multiply, 4096³&lt;/td&gt;
&lt;td&gt;9.4 TFLOPS&lt;/td&gt;
&lt;td&gt;9.8 TFLOPS&lt;/td&gt;
&lt;td&gt;5.0 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matrix multiply, MLP-shaped (8448×3072×12288)&lt;/td&gt;
&lt;td&gt;9.7 TFLOPS&lt;/td&gt;
&lt;td&gt;10.2 TFLOPS&lt;/td&gt;
&lt;td&gt;4.5 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attention, 24 heads × 8448 tokens × 128&lt;/td&gt;
&lt;td&gt;5.94 TFLOPS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.24 TFLOPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.67 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3×3 convolution, 128 channels at 512²&lt;/td&gt;
&lt;td&gt;16.8 TFLOPS&lt;/td&gt;
&lt;td&gt;13.3 TFLOPS&lt;/td&gt;
&lt;td&gt;3.4 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elementwise multiply, 2²⁸ values&lt;/td&gt;
&lt;td&gt;24.9 G/s&lt;/td&gt;
&lt;td&gt;22.8 G/s&lt;/td&gt;
&lt;td&gt;45.9 G/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;(The FP32 and FP16 convolution figures exceed the card's 11.8 TFLOPS peak because cuDNN picks a fast algorithm such as Winograd, which does fewer multiplies than the nominal count I divided by.)&lt;/p&gt;
&lt;p&gt;The profiler shows why FP16 matrix multiplies run at full speed: cuBLAS on Pascal uses kernels named &lt;code&gt;maxwell_fp16_sgemm_fp16&lt;/code&gt;, which read FP16, compute in FP32, and write FP16. The data is half precision; the arithmetic is not. Speed varies by shape (one FP16 down-projection shape ran at 2.8 TFLOPS through &lt;code&gt;F.linear&lt;/code&gt;), but nothing like 1/64.&lt;/p&gt;
&lt;p&gt;One row in that table is 25 times slower than FP32: attention.&lt;/p&gt;
&lt;h3&gt;One attention kernel&lt;/h3&gt;
&lt;p&gt;PyTorch's &lt;code&gt;scaled_dot_product_attention&lt;/code&gt; picks a backend at runtime. On a P40 in FP16 it picks the memory-efficient kernel, &lt;code&gt;fmha_cutlassF_f16_aligned_32x128_rf_sm50&lt;/code&gt;, and that kernel takes 3.69 seconds for an attention call that takes 0.148 seconds in FP32. It evidently does at least part of its arithmetic in FP16, which this chip runs at 1/64 speed. Forcing the plain "math" backend, which upcasts to FP32 and runs ordinary matrix multiplies, brings the same call down to 0.328 seconds.&lt;/p&gt;
&lt;p&gt;On real FireRed blocks, that one kernel is 89% of the GPU time in FP16: 7.40 of 8.32 seconds across two blocks. Everything else in an FP16 block is faster than FP32.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Six real FireRed blocks, one P40&lt;/th&gt;
&lt;th&gt;Per block&lt;/th&gt;
&lt;th&gt;Output vs FP32&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FP32, stock&lt;/td&gt;
&lt;td&gt;1.08 s&lt;/td&gt;
&lt;td&gt;reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP16, stock&lt;/td&gt;
&lt;td&gt;4.17 s&lt;/td&gt;
&lt;td&gt;every value inf or NaN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP16, math attention forced&lt;/td&gt;
&lt;td&gt;0.92 s&lt;/td&gt;
&lt;td&gt;every value inf or NaN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BF16 (math attention is automatic)&lt;/td&gt;
&lt;td&gt;0.86 s&lt;/td&gt;
&lt;td&gt;0.8% relative error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Scaled to a full denoising step (60 blocks, two passes for classifier-free guidance), stock FP16 comes to about 500 seconds, 8.3 minutes. February's measured "approximately 9 minutes per step" sits right next to it, which is the best evidence I have that this kernel was February's problem too.&lt;/p&gt;
&lt;p&gt;Fixing the speed does not rescue FP16 for this model. After six blocks, all 524,288 output values had overflowed. These blocks were fed random inputs rather than real text embeddings, so I would not read too much into exactly where it breaks; the February pipeline, with real inputs, produced a solid black image for the same reason. FP16's maximum value is 65,504, and FireRed's activations exceed it.&lt;/p&gt;
&lt;h3&gt;BF16 runs on a P40&lt;/h3&gt;
&lt;p&gt;Several of my earlier P40 posts say the card cannot compute in BF16. That is half right. Pascal has no BF16 arithmetic, but PyTorch and cuBLAS treat BF16 as a storage format: the matrix multiplies run through &lt;code&gt;magma_sgemmEx&lt;/code&gt; kernels that read BF16 and compute in FP32. BF16 has FP32's exponent range, so nothing overflowed, and its error against FP32 after six blocks was 0.8%.&lt;/p&gt;
&lt;p&gt;The costs are real but specific. BF16 matrix multiplies run at about half the FP32 rate. BF16 convolutions run at a fifth of it, so the VAE decode of a 1024x1024 image took 3.3 seconds in BF16 against 1.8 in FP32. (FP16 took 3.5 seconds there and, unlike the transformer, decoded cleanly: 0.03/255 average difference from FP32.) And there is no memory-efficient attention kernel for BF16 on Pascal, so PyTorch falls back to the math backend, which materializes the full attention matrix: six BF16 blocks peaked at 20.5 GB against 9.9 GB for six FP32 blocks. The March home-lab post's figure of "roughly 21% slower" does not match any of these measurements; the right answer depends entirely on which operation you are running.&lt;/p&gt;
&lt;h3&gt;FP32 was running at half speed&lt;/h3&gt;
&lt;p&gt;BF16 beating FP32 on the blocks made no sense if BF16 matrix multiplies are half as fast, so I profiled FP32. Inside the model, its matrix multiplies averaged 2.3 TFLOPS, about a quarter of what the same shapes reach in isolation. The difference is the bias.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nn.Linear&lt;/code&gt; calls &lt;code&gt;F.linear(x, weight, bias)&lt;/code&gt;, and PyTorch hands the bias to cuBLAS to fuse into the matrix multiply. For FP32 on Pascal, that fused path selects a different kernel, &lt;code&gt;maxwell_sgemm_32x128_tn&lt;/code&gt;, which runs at 3.9 TFLOPS. The same multiply without the bias selects &lt;code&gt;sgemm_128x128x8_NT_vec&lt;/code&gt; and runs at 8.5 TFLOPS:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FP32 linear layer, 8192 tokens, 3072→12288&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;F.linear(x, w, b)&lt;/code&gt; (bias fused)&lt;/td&gt;
&lt;td&gt;157 ms&lt;/td&gt;
&lt;td&gt;3.9 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;F.linear(x, w) + b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;73–77 ms&lt;/td&gt;
&lt;td&gt;8.0–8.5 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In that test the two outputs were bit-identical. PyTorch 2.10 with CUDA 12.6 behaves the same as 2.6. FP16 is not affected: with or without the bias, the same shape takes 79 ms.&lt;/p&gt;
&lt;p&gt;The fix is a replacement &lt;code&gt;forward&lt;/code&gt; for &lt;code&gt;nn.Linear&lt;/code&gt;. Apply it before loading the model, because accelerate's device hooks capture &lt;code&gt;forward&lt;/code&gt; on some modules when a model is spread across GPUs:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch.nn.functional&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;F&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;linear_forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&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;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bias&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bias&lt;/span&gt;

&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Linear&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;linear_forward&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On six FireRed blocks it took FP32 from 1.08 to 0.53 seconds per block, 2.06 times faster, with a relative difference from the stock output of 6×10⁻⁷ and the same 9.9 GB peak memory. On these blocks, split-bias FP32 beats BF16 by 1.6x and stock FP16 by 7.9x, and it is the only configuration that is both the fastest and numerically clean.&lt;/p&gt;
&lt;h3&gt;The full pipeline&lt;/h3&gt;
&lt;p&gt;Block numbers are extrapolations, so I ran the real thing: FireRed-Image-Edit end to end with February's inputs (the same photo, "Add a red hat on the cat," seed 43, true CFG at 4.0). The FP32 transformer is 82 GB, so it is split across all four P40s, 12 blocks on the first card and 16 on each of the others, and every card peaked between 21.0 and 21.8 GiB. As in February, the text encoder ran once in FP16 and was freed before the transformer loaded. The final VAE decode ran on the CPU, so a 40-step run could not run out of GPU memory at the very end.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FireRed-Image-Edit, four P40s&lt;/th&gt;
&lt;th&gt;Per step&lt;/th&gt;
&lt;th&gt;40 steps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FP32, stock (5-step run)&lt;/td&gt;
&lt;td&gt;144.2 s&lt;/td&gt;
&lt;td&gt;about 96 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FP32, bias split out (40-step run)&lt;/td&gt;
&lt;td&gt;62.8 s&lt;/td&gt;
&lt;td&gt;41.9 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;February: INT8 transformer, FP32 everything else&lt;/td&gt;
&lt;td&gt;87.5 s&lt;/td&gt;
&lt;td&gt;58 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/firered-p40-fp32-splitbias-40step.png.webp" alt="The 40-step FP32 split-bias output on four P40s: a cat in a red knit hat added to the cover of the Python book" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; padding: 20px;" loading="lazy"&gt;&lt;/p&gt;
&lt;p&gt;The steps barely vary: 144.2 to 144.3 seconds stock, 62.6 to 63.0 seconds with the fix. After five steps, the split-bias image differed from the stock one by 0.005/255 per pixel on average and 6/255 at most; the two matrix-multiply kernels add in a different order, so the results are not bit-identical, but they are the same picture. The 40-step result is the same red hat on the same cat that February's INT8 run produced.&lt;/p&gt;
&lt;p&gt;Split-bias FP32 takes 28% less time per step than February's INT8 configuration on the same cards, with no quantization at all.&lt;/p&gt;
&lt;p&gt;Stock FP32 came in slower than my block-level estimate, 144 seconds against 130. The block test leaves out the input and output layers, three hand-offs between cards on every pass, and the scheduler; that is where the other 14 seconds go.&lt;/p&gt;
&lt;h3&gt;And the Strix Halo&lt;/h3&gt;
&lt;p&gt;February's headline was that a single AMD Strix Halo APU ran this model 6% faster than all four P40s: 82.6 seconds per step against 87.5. If the P40s now take 62.8, that headline flips, but only if the Strix is held to the same standard. The P40s just got tuned. So did the Strix: same photo, prompt, seed and guidance, the whole transformer in BF16 as in February, five steps per configuration, timed the same way.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FireRed-Image-Edit, one Strix Halo (BF16)&lt;/th&gt;
&lt;th&gt;Per step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;February's software (PyTorch 2.7.1, ROCm 7.9 RC1), stock&lt;/td&gt;
&lt;td&gt;106.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same, bias split out&lt;/td&gt;
&lt;td&gt;106.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same, AOTriton attention enabled&lt;/td&gt;
&lt;td&gt;106.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newer software (PyTorch 2.9.1, ROCm 7.11 RC2), stock&lt;/td&gt;
&lt;td&gt;107.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;newer, AOTriton attention enabled&lt;/td&gt;
&lt;td&gt;21.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;newer, AOTriton plus bias split out&lt;/td&gt;
&lt;td&gt;20.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;newer, AOTriton plus TunableOp&lt;/td&gt;
&lt;td&gt;20.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;newer, AOTriton, TunableOp and bias split out&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.4 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;One setting does almost all of it. &lt;code&gt;TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1&lt;/code&gt; lets PyTorch use AOTriton's flash-attention kernels on this GPU instead of the math fallback, and on the newer software it takes a step from 107.7 seconds to 21.6 and peak memory from 52.9 GiB to 38.9. February's software accepts the variable and ignores it. TunableOp, which times the available matrix-multiply kernels for each shape and keeps the fastest, takes another 5%; the first step pays for the search (one to three minutes) and the rest reuse it. Splitting out the bias, the P40's big win, is worth nothing on February's ROCm stack and 7% on the newer one: it is a cuBLAS kernel-selection problem, not a PyTorch one.&lt;/p&gt;
&lt;p&gt;Fully tuned, the Strix Halo is 3.2 times faster than the four tuned P40s, 19.4 seconds per step against 62.8. February's 6% was a comparison between two untuned machines, and the untuned Strix was leaving more on the table than the untuned P40s were. The P40 fix is real and worth having. It does not change who wins.&lt;/p&gt;
&lt;p&gt;Two things here do not reconcile, and I am not going to paper over them. First, February's software, run today, takes 106.9 seconds per step, not February's 82.6. The one difference I can see is the GPU memory configuration: in February the pipeline used about 75 GB of GPU memory, and today PyTorch sees 59.6 GiB, of which only 4 GiB is a dedicated carve-out, with the rest borrowed from system memory. I have not changed the BIOS to test February's setting, so the 29% gap is unexplained. Second, that memory limit is why the text encoder and the VAE run on the CPU in these tests; the whole BF16 pipeline (57.7 GB) no longer fits. Neither runs inside the timed denoising loop, so the per-step numbers compare like with like, but it is not February's exact setup. The images agree: every configuration produced the same red hat on the same cat, with BF16 and different kernels drifting by 1.5 to 2.7/255 per pixel on average after five steps.&lt;/p&gt;
&lt;h3&gt;Caveats&lt;/h3&gt;
&lt;p&gt;The block tests use FireRed's real weights but random inputs, and they time 6 of 60 blocks; the per-step figures for anything other than the full pipeline runs are extrapolations that leave out the text encoder, the VAE, and transfers between cards. The P40 runs used PyTorch 2.6.0 with CUDA 12.4 on driver 580.178.04 (the bias result also holds on PyTorch 2.10 with CUDA 12.6); the Strix runs used the two ROCm stacks in the table, with the same diffusers, transformers and accelerate versions. The Strix numbers are five-step runs, and the P40 runs are FP32 across four cards while the Strix runs are BF16 on one APU: each is that machine's fastest correct configuration, not the same arithmetic. I did not test training, other models, or newer GPUs, which have FP16 and BF16 hardware and will rank the three precisions differently. The bias result is a cuBLAS kernel-selection behavior, and a future cuBLAS could change it.&lt;/p&gt;
&lt;h3&gt;What I would do on a P40 now&lt;/h3&gt;
&lt;p&gt;Run FP32 and patch &lt;code&gt;nn.Linear&lt;/code&gt; to split out the bias. If a model does not fit in FP32, store it in BF16 rather than FP16, because BF16 will not overflow, and budget for the math-attention memory. If you must use FP16, force the math attention backend with &lt;code&gt;torch.nn.attention.sdpa_kernel(SDPBackend.MATH)&lt;/code&gt; and check the output for inf and NaN, because the speed fix does nothing for the range problem.&lt;/p&gt;
&lt;p&gt;The March &lt;a href="https://tinycomputers.io/posts/running-ltx-video-on-four-tesla-p40s.html"&gt;LTX-Video run on these same four cards&lt;/a&gt; patched &lt;code&gt;F.linear&lt;/code&gt; to upcast each weight and bias to FP32 and then call the original &lt;code&gt;F.linear&lt;/code&gt; with the bias attached, which is exactly the slow path. It probably paid this penalty on every linear layer. I have not re-run it.&lt;/p&gt;</description><category>ai hardware</category><category>attention</category><category>benchmarks</category><category>bf16</category><category>cublas</category><category>diffusion models</category><category>firered</category><category>fp16</category><category>fp32</category><category>gpu computing</category><category>nvidia p40</category><category>pascal</category><category>pytorch</category><guid>https://tinycomputers.io/posts/why-fp16-is-slow-on-a-tesla-p40.html</guid><pubDate>Sat, 31 Oct 2026 21:30:00 GMT</pubDate></item></channel></rss>