Skip to content

Givens orthogonal layer#57

Open
VolodyaCO wants to merge 1 commit into
dwavesystems:mainfrom
VolodyaCO:givens-rotation
Open

Givens orthogonal layer#57
VolodyaCO wants to merge 1 commit into
dwavesystems:mainfrom
VolodyaCO:givens-rotation

Conversation

@VolodyaCO

Copy link
Copy Markdown
Collaborator

This PR adds an orthogonal layer given by Givens rotations, using the parallel algorithm described by Firas in https://arxiv.org/abs/2106.00003, which gives a forward complexity of O(n) and backward complexity of O(n log(n)), even though there are O(n^2) rotations.

This PR still is in draft. I wrote it for even n. Probably some more unit tests are to be done, but I am quite lazy (will do it after all math is checked for odd n).

@VolodyaCO VolodyaCO requested a review from kevinchern December 19, 2025 00:01
@VolodyaCO VolodyaCO self-assigned this Dec 19, 2025
@VolodyaCO VolodyaCO added the enhancement New feature or request label Dec 19, 2025
@VolodyaCO

Copy link
Copy Markdown
Collaborator Author

I somehow broke @kevinchern's tests, what the hell...

Comment thread tests/helper_models.py Outdated
Comment thread tests/test_nn.py
@kevinchern

kevinchern commented Dec 19, 2025

Copy link
Copy Markdown
Collaborator

I somehow broke @kevinchern's tests, what the hell...

@VolodyaCO which tests? I'm seeing test_forward_agreement and test_backward_agreement failures on this CI test

@VolodyaCO

Copy link
Copy Markdown
Collaborator Author

I somehow broke @kevinchern's tests, what the hell...

@VolodyaCO which tests? I'm seeing test_forward_agreement and test_backward_agreement failures on this CI test

I forgot to update my tests to float64 precision. Now that I've done it, it's weird that all of the current failing tests are failing on

  File "/Users/distiller/project/tests/test_nn.py", line 144, in test_LinearBlock
    self.assertTrue(model_probably_good(model, (din,), (dout,)))

@kevinchern

Copy link
Copy Markdown
Collaborator

I somehow broke @kevinchern's tests, what the hell...

@VolodyaCO which tests? I'm seeing test_forward_agreement and test_backward_agreement failures on this CI test

I forgot to update my tests to float64 precision. Now that I've done it, it's weird that all of the current failing tests are failing on

  File "/Users/distiller/project/tests/test_nn.py", line 144, in test_LinearBlock
    self.assertTrue(model_probably_good(model, (din,), (dout,)))

Ahhhhhh. OK Theo also flagged this at #50 . It's a poorly-written test.. you can ignore it.

@VolodyaCO VolodyaCO marked this pull request as ready for review December 26, 2025 17:28
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py
Comment on lines +122 to +127
angles, blocks, Ufwd_saved = ctx.saved_tensors
Ufwd = Ufwd_saved.clone()
M = grad_output.t() # dL/dU, i.e., grad_output is of shape (n, n)
n = M.size(1)
block_size = n // 2
A = torch.zeros((block_size, n), device=angles.device, dtype=angles.dtype)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here re lowercase for Ufwd, M, and A. Avoids incorrect colour highlighting in themes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I didn't read this about the incorrect colour highlighting before I made my previous comment. I still think that it is easier to read the algorithm alongside the code if the use of lower/upper case match. For example, lower case m is usually used for an integer variable, not a tensor.

Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread tests/helper_models.py Outdated
Comment thread tests/test_nn.py Outdated
@VolodyaCO

Copy link
Copy Markdown
Collaborator Author

After a bit of git wrangling, I was able to clean my whole mess of merge commits 😆.

@anahitamansouri anahitamansouri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice PR Vlad. It took me a while to go over the paper and this PR :) The only thing is the tests that are failing. Thanks for the great work.

Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated

@kevinchern kevinchern left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a quick pass to provide some feedback before taking some time to take a deep dive into the paper.

Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
for _ in range(n - 1):
pairs = circle_method(sequence)
if is_odd:
# Remove pairs involving the dummy dimension:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Remove pairs involving the dummy dimension:
# Remove pairs involving the dummy dimension

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was gonna ask why remove the colon?

Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
@VolodyaCO VolodyaCO force-pushed the givens-rotation branch 3 times, most recently from 1146ebb to df400fb Compare January 14, 2026 14:41
@kevinchern

Copy link
Copy Markdown
Collaborator

@VolodyaCO can you rebase on main?

Comment thread dwave/plugins/torch/nn/modules/orthogonal.py
Comment thread tests/helper_models.py Outdated
Comment thread tests/test_nn.py
@VolodyaCO VolodyaCO force-pushed the givens-rotation branch 3 times, most recently from 6587b89 to 97c2b8e Compare April 27, 2026 21:33
@kevinchern kevinchern closed this Apr 27, 2026
@kevinchern kevinchern reopened this Apr 27, 2026

@VolodyaCO VolodyaCO left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinchern I have addressed all comments. Should be ready to merge now.

Comment thread tests/test_nn.py
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py
Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
__all__ = ["GivensRotation"]


class _RoundRobinGivens(torch.autograd.Function):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation is quite thorough for a hidden class. Would this perhaps make sense moving to a nn.functions namespace and removing the underscore? @kevinchern

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kevinchern bumping this

Comment thread dwave/plugins/torch/nn/modules/orthogonal.py Outdated
Comment on lines +98 to +101
# Initialize U^fwd from forward pass output U. Mathematically, U^fwd represents U^{1:k-1}
# at block k, defined in equation (11). It is post-multiplied by G_bk^T at each block
# iteration to "remove the effect of the block's rotations" (Section 4.1, paragraph before
# equation (12)). This corresponds to the update from U^{1:k} back to U^{1:k-1}.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at the reference link, but these comments seem a bit messy (for lack of a better term). If the algorithm in the link describes the process in a clear way, I'd perhaps shorten these comments a bit, although references to equations are always good.

Also, separation by empty lines always makes things look a bit tidier.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added separation by empty lines to make things look tidier. I added these inline comments at @kevinchern 's request. I do agree that it is enough for the interested reader to read the paper. I also think it's helpful to have the inline comments too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shorten these comments a bit

I second this opinion

kevin's request

To clarify my suggestion: it will be very helpful for future maintenance to include references to "where" these equations appear. The idea is for the developer to be able to easily reference the material and not necessarily understand the method from comments.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point. Comments have now been shortened.

@kevinchern kevinchern left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests hinge on NaiveGivensRotationLayer being correct but there aren't correctness tests for it. It checks for serial versus parallel implementations.
We need a unit test to check the forward and backward methods are correct, e.g., work out the expected forward/backward values a small input (n=3?)

Comment thread tests/test_nn.py Outdated
Sequentially applies all Givens rotations to implement an orthogonal transformation in an order
provided by blocks, which are of shape (n_blocks, n/2, 2), and where usually each block contains
pairs of indices such that no index appears more than once in a block. However, this
implementation does not rely on that assumption, so that indeces can appear multiple times in a

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo indeces

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread tests/test_nn.py
from tests.helper_functions import model_probably_good


class NaiveGivensRotationLayer(nn.Module):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this class in tests?
My understanding is its purpose is to test the parallel implementation, which is error-prone in implementation.
If that's correct, I would note its purpose in the docstring.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the docstring to note its purpose.

Comment thread tests/test_nn.py

import torch
import torch.nn as nn
from einops import einsum

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use einops.einsum instead of torch.einsum?

@kevinchern kevinchern May 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did a quick google search and it suggests torch.einsum has the advantage of being torch-native, so it benefits from torch.compile whereas einops is more general but less optimized for torch.

einops is generally useful and i forget if it's used elsewhere in the package---if you revert this to torch.einsum (I think we should), remove einops from requirements.txt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vaguely recall Theo commented on einops as a requirement when this PR first came up, and I mentioned I also use it (einops.rearrange). Can't seem to find that discussion...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/arogozhnikov/einops/wiki/Using-torch.compile-with-einops

They are compatible. einops is used in tests, repeating and rearranging and indeed better written in einops than pytorch. It's just a clearer syntax—easier to read.

Comment thread tests/test_nn.py Outdated
# Test orthogonality:
I = torch.eye(n, dtype=U_parallel.dtype)
UU_T = U_parallel @ U_parallel.T
self.assertTrue(torch.allclose(I, UU_T, atol=1e-6))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this in a subtest

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread tests/test_nn.py Outdated
U_parallel = layer._create_rotation_matrix()

# Test that the matrices are close
self.assertTrue(torch.allclose(U_naive, U_parallel, atol=1e-6))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this in a subtest

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread tests/test_nn.py Outdated
U_parallel = layer._create_rotation_matrix()

# Test that the matrices are close
self.assertTrue(torch.allclose(U_naive, U_parallel, atol=1e-6))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, if I understood correctly, this test is redundant given the next test (identity test).
Good to keep though!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not equivalent tests. One tests orthogonality, the other tests that both non-parallel and parallel implementations provide the same matrix.

Comment on lines +68 to +76
angles_in_block = angles[idx_block + b * block_size] # shape (n/2,)
c = torch.cos(angles_in_block).unsqueeze(0)
s = torch.sin(angles_in_block).unsqueeze(0)
i_idx = block[:, 0]
j_idx = block[:, 1]
r_i = c * U[:, i_idx] + s * U[:, j_idx]
r_j = -s * U[:, i_idx] + c * U[:, j_idx]
U[:, i_idx] = r_i
U[:, j_idx] = r_j

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am stating in the docstring that this refers to Algorithm 2, which is where all of these instructions are written. I think this is sufficient for the reader to establish a way of reading the paper that corresponds to the written code.

Comment on lines +98 to +101
# Initialize U^fwd from forward pass output U. Mathematically, U^fwd represents U^{1:k-1}
# at block k, defined in equation (11). It is post-multiplied by G_bk^T at each block
# iteration to "remove the effect of the block's rotations" (Section 4.1, paragraph before
# equation (12)). This corresponds to the update from U^{1:k} back to U^{1:k-1}.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shorten these comments a bit

I second this opinion

kevin's request

To clarify my suggestion: it will be very helpful for future maintenance to include references to "where" these equations appear. The idea is for the developer to be able to easily reference the material and not necessarily understand the method from comments.

# M represents the product U^bck @ Γ^T. From equation (15): M ≡ U^bck @ Γ^T, where
# U^bck = U^{k:n-1} (the product of remaining blocks, defined in equation (11)).
# At each block k, M is pre-multiplied by G_bk to advance from U^{k+1:n-1} to U^{k:n-1}.
M = grad_output.t()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to use tensor.t() instead of tensor.T? I suggest picking one and using them consistently throughout.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. Going for .T

Comment on lines +192 to +200
self.n = n
self.n_angles = n * (n - 1) // 2
self.angles = nn.Parameter(torch.randn(self.n_angles))
blocks_edges = self._get_blocks_edges(n)
self.register_buffer("blocks", blocks_edges)
if bias:
self.bias = nn.Parameter(torch.zeros(n))
else:
self.register_parameter("bias", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should some of these be made private and, if needed, accessed via @property?
cc @thisac

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do that, to signal that they shouldn't be changed by user. 👍

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Address orthogonal module PR feedback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants