Why does rotating both input and kernel not give rotated output in conv2d?

Alejandro_Garcia: <p>Hi,</p>
<p>I have the following minimal code example:</p>
<pre><code class="lang-auto">import torch 
import torch.nn.functional as F  

x = torch.rand(1 , 1, 100, 100) - 0.5 
w = torch.rand(1 , 1, 5, 5) - 0.5  
y1 = F.conv2d(x, w, stride=1, padding=0)  
x90 = torch.rot90(x, 1, (2,3)) 
w90 = torch.rot90(w, 1, (2,3)) 
y2 = F.conv2d(x90, w90, stride=1, padding=0)  
y1_rot = torch.rot90(y1, 1, (2,3))  
print(torch.allclose(y2, y1_rot))  # returns False  
</code></pre>
<p>My expectation:</p>
<ul>
<li>
<p>If I rotate the input by 90° and also rotate the kernel by 90°, then apply convolution, the result should be the rotated version of the original convolution output.</p>
</li>
<li>
<p>In other words, I expected <code>y2 == rot90(y1)</code> up to floating point tolerance.</p>
</li>
</ul>
<p>But in practice the code prints <code>False</code>.</p>
<p>Is this behavior expected in <code>torch.nn.functional.conv2d</code>?</p>
<p>Thanks in advance!</p>

ptrblck: <p>The testing thresholds might be too strict as it seems the <code>allclose</code> check passes with <code>atol=1e-7</code>.</p>

Alejandro_Garcia: <p>Thanks! You’re right.<br>
In this case it was the threshold. Previously I had larger differences due to the stride (2,2) when the image had even dimensions.</p>
<p>Now the differences are very small, which are negligible in a single layer, but become more important when stacking multiple layers.</p>
<p>Something that caught my attention is that the cosine similarity is always greater between 180° rotations than between 90° rotations. Any idea how to analyze this?</p>
<p>Thanks again!</p>

Alejandro_Garcia: <p>Here’s an example where you can see the behavior I mentioned before.</p>
<pre><code class="lang-auto">import numpy as np
import torch
import torch.nn.functional as F
from matplotlib import pyplot as plt
import seaborn as sns

s = 1
p = 0
err = np.zeros((4,4))

for a in range(1000):
  x0 = torch.rand(1 , 1, 100, 100)-0.5
  x90 = torch.rot90(x0, 1, (2,3))
  x180 = torch.rot90(x0, 2, (2,3))
  x270 = torch.rot90(x0, 3, (2,3))

  w0 = torch.rand(1 , 1, 5, 5)-0.5
  w90 = torch.rot90(w0, 1, (2,3))
  w180 = torch.rot90(w0, 2, (2,3))
  w270 = torch.rot90(w0, 3, (2,3))

  y0 = F.conv2d(x0, w0, stride=s, padding=p)
  y90 = F.conv2d(x90, w90, stride=s, padding=p)
  y180 = F.conv2d(x180, w180, stride=s, padding=p)
  y270 = F.conv2d(x270, w270, stride=s, padding=p)

  y90_r = torch.rot90(y90, -1, (2,3))
  y180_r = torch.rot90(y180, -2, (2,3))
  y270_r = torch.rot90(y270, -3, (2,3))

  outputs = [y0, y90_r, y180_r, y270_r]
  for i in range(4):
    for j in range(4):
      err[i,j] = err[i,j] + torch.abs(outputs[i] - outputs[j]).sum()
print(err)
plt.figure(figsize=(3,3))
sns.heatmap(err, cmap='gray', cbar=False)
plt.show()
</code></pre>
<p>You’ll always see the same pattern show up in the error matrix. The largest errors are at (0,2) and (1,3).</p>
<p><div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/3X/6/a/6a821bd9b098c70f543bb7fe5f2c24f1a8f30667.png" data-download-href="https://discuss.pytorch.org/uploads/default/6a821bd9b098c70f543bb7fe5f2c24f1a8f30667" title="Screenshot from 2025-10-01 14-58-35"><img src="https://discuss.pytorch.org/uploads/default/original/3X/6/a/6a821bd9b098c70f543bb7fe5f2c24f1a8f30667.png" alt="Screenshot from 2025-10-01 14-58-35" data-base62-sha1="fcdmR9j9BJGe6gVfQFGU0ej4r6T" width="218" height="214"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use href="#far-image"></use></svg><span class="filename">Screenshot from 2025-10-01 14-58-35</span><span class="informations">218×214 1.11 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use href="#discourse-expand"></use></svg></div></a></div></p>