<?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: b1ackjack12</title>
    <description>The latest articles on DEV Community by b1ackjack12 (@b1ackjack12).</description>
    <link>https://dev.to/b1ackjack12</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%2F4178028%2F73382ce8-418c-4b2f-8812-be886daa9b88.png</url>
      <title>DEV Community: b1ackjack12</title>
      <link>https://dev.to/b1ackjack12</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/b1ackjack12"/>
    <language>en</language>
    <item>
      <title>I Set Every Seed and Trained Twice. The Models Diverged at Step 1</title>
      <dc:creator>b1ackjack12</dc:creator>
      <pubDate>Mon, 12 Oct 2026 01:16:31 +0000</pubDate>
      <link>https://dev.to/b1ackjack12/i-set-every-seed-and-trained-twice-the-models-diverged-at-step-1-2h5b</link>
      <guid>https://dev.to/b1ackjack12/i-set-every-seed-and-trained-twice-the-models-diverged-at-step-1-2h5b</guid>
      <description>&lt;p&gt;There's a ritual at the top of every training script, mine included: &lt;code&gt;torch.manual_seed(0)&lt;/code&gt;, &lt;code&gt;np.random.seed(0)&lt;/code&gt;, and the comfortable belief that the run is now reproducible. I've typed it for years. Last week, while chasing a one-point accuracy difference I couldn't explain, I finally tested the belief directly: same seed, same data order, same model init, same machine, same everything — train ResNet18 for 200 steps, twice, and compare.&lt;/p&gt;

&lt;p&gt;The two loss curves matched at step 0 and &lt;strong&gt;diverged at step 1&lt;/strong&gt;. After 200 steps, the largest difference between corresponding weights was 0.237 — for parameters that mostly live between −0.1 and 0.1, that's not drift, that's a different model. Both runs were "seeded." Neither was reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the randomness sneaks in
&lt;/h2&gt;

&lt;p&gt;The seeds did their job: initialization, data order, and every explicit random draw were identical, which is why step 0 agreed perfectly. The divergence enters in the &lt;em&gt;backward pass&lt;/em&gt;, and it isn't randomness in the usual sense at all — it's floating-point arithmetic meeting parallel hardware.&lt;/p&gt;

&lt;p&gt;Floating-point addition is not associative: &lt;code&gt;(a + b) + c&lt;/code&gt; and &lt;code&gt;a + (b + c)&lt;/code&gt; can differ in the last bit. GPU kernels sum gradient contributions across thousands of threads, many using atomic operations whose completion &lt;em&gt;order depends on the scheduler&lt;/em&gt; — which threads won the race on this particular run, on this particular clock. Same numbers, different addition order, last-bit differences in the gradients. Then SGD amplifies them: a 1e-8 disagreement in a gradient becomes a slightly different weight, which becomes a different activation, and the gap compounds every step. By my run's end, noise that began around the last decimal place a float can represent had grown into 0.237. It's the butterfly effect with a commit hash.&lt;/p&gt;

&lt;p&gt;None of this is a PyTorch bug; it's documented behavior, and &lt;code&gt;cudnn.benchmark&lt;/code&gt; (which I've &lt;a href="https://b1ackjack12.github.io/posts/tf32-cudnn-benchmark-measured" rel="noopener noreferrer"&gt;previously measured&lt;/a&gt; and left enabled) adds a second layer by autotuning to potentially different — nondeterministic — conv algorithms per run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl3sjg27j1wjq8tdv65ko.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl3sjg27j1wjq8tdv65ko.webp" alt="Two identical glowing racing lines leaving one starting gate and slowly fanning apart into visibly different paths across a dark circuit board landscape" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Does 0.237 matter? Only when you're asking small questions
&lt;/h2&gt;

&lt;p&gt;For final accuracy, this nondeterminism behaves like one more seed you didn't choose — my &lt;a href="https://b1ackjack12.github.io/posts/batch-level-gpu-augmentation-accuracy" rel="noopener noreferrer"&gt;augmentation experiment&lt;/a&gt; put ordinary seed-to-seed spread at ±0.3–0.5 points on a small CIFAR setup, and same-seed GPU noise lands in the same statistical bucket. If your effect size dwarfs that, you can ignore this whole post.&lt;/p&gt;

&lt;p&gt;The cases where it bites are the small questions, which are most of debugging. "Did my refactor change the numerics?" — you can't diff against the pre-refactor run, because &lt;em&gt;nothing&lt;/em&gt; diffs clean, ever. "Is this 0.4-point improvement my new loss term or luck?" — under nondeterminism you can't even hold the luck constant while you toggle the loss term. A/B comparisons where the honest answer requires the same noise on both sides are exactly where "seeded but nondeterministic" quietly wastes days. I know because the one-point mystery that started this post turned out to be two runs of the &lt;em&gt;same code&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and its real price
&lt;/h2&gt;

&lt;p&gt;PyTorch ships the switch; it just isn't the seed:&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;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use_deterministic_algorithms&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;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cudnn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="c1"&gt;# and, for CUDA &amp;gt;= 10.2, before the process starts:
# CUBLAS_WORKSPACE_CONFIG=:4096:8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With those set, my two 200-step runs matched &lt;strong&gt;bit-for-bit&lt;/strong&gt;: max weight difference exactly 0, every loss value identical to the last digit. Two footnotes from the setup: the &lt;code&gt;CUBLAS_WORKSPACE_CONFIG&lt;/code&gt; environment variable is mandatory (you get a clear RuntimeError without it), and &lt;code&gt;use_deterministic_algorithms&lt;/code&gt; will refuse to run ops that simply have no deterministic GPU implementation — ResNet18 sailed through, but if your model contains one of the holdouts, the flag fails loudly and your options are CPU fallback or living without determinism for that op. Loud failure, for once, is the feature.&lt;/p&gt;

&lt;p&gt;The folklore price for all this is "2–3x slower," usually delivered as a reason not to bother. Measured, on the ResNet18 training step:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;th&gt;default&lt;/th&gt;
&lt;th&gt;deterministic&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fp32&lt;/td&gt;
&lt;td&gt;17.94 ms&lt;/td&gt;
&lt;td&gt;20.90 ms&lt;/td&gt;
&lt;td&gt;+16.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bf16 autocast&lt;/td&gt;
&lt;td&gt;12.42 ms&lt;/td&gt;
&lt;td&gt;14.37 ms&lt;/td&gt;
&lt;td&gt;+15.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sixteen percent. Some of that isn't even determinism proper — it includes giving up &lt;code&gt;cudnn.benchmark&lt;/code&gt;, which &lt;a href="https://b1ackjack12.github.io/posts/tf32-cudnn-benchmark-measured" rel="noopener noreferrer"&gt;I measured at ~3%&lt;/a&gt; on this model. The 2–3x stories presumably come from specific unlucky ops or older stacks, and maybe your model contains one; that's what the two-minute measurement is for. On mine, bit-exact reproducibility costs about as much as a modestly larger batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the switch sits now
&lt;/h2&gt;

&lt;p&gt;My default stays fast and nondeterministic — for training throughput, 16% is real money, and ordinary training doesn't need bit-exactness. But the flag has earned a permanent spot in three situations: any debugging session that compares two runs (which is to say, any debugging session), numerics-sensitive refactors where I want &lt;code&gt;git diff&lt;/code&gt;-grade certainty that outputs didn't change, and small-effect experiments where the noise floor would otherwise eat the result. The deciding question is one sentence: &lt;em&gt;am I about to compare two runs and interpret the difference?&lt;/em&gt; If yes, 16% is the cheapest insurance in this entire optimization series — and unlike the seeds ritual, it actually buys what it claims to.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://b1ackjack12.github.io/posts/pytorch-same-seed-different-model/" rel="noopener noreferrer"&gt;b1ackjack12.github.io&lt;/a&gt; — an AI tech blog where every claim comes with a measurement. More experiments like this one live there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pytorch</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>reproducibility</category>
    </item>
  </channel>
</rss>
