Skip to content

Fix LinearOperatorInversion.solve with an operator and adjoint=True - #129837

Merged
copybara-service[bot] merged 2 commits into
tensorflow:masterfrom
b84330808:fix-inversion-solve-adjoint
Nov 9, 2026
Merged

copybara-service[bot] merged 2 commits into
tensorflow:masterfrom
b84330808:fix-inversion-solve-adjoint

Conversation

@b84330808

Copy link
Copy Markdown
Contributor

tf.linalg.LinearOperatorInversion(A).solve(rhs_operator, adjoint=True) silently returns A^-1 B instead of A^H B.

import tensorflow as tf
A = tf.constant([[1., 2.], [3., 4.]])
B = tf.constant([[5., 6.], [7., 8.]])
inv = tf.linalg.LinearOperatorInversion(tf.linalg.LinearOperatorFullMatrix(A))
inv.solve(tf.linalg.LinearOperatorFullMatrix(B), adjoint=True).to_dense()
# [[-3, -4], [4, 5]]    <- A^-1 B
inv.solve(B, adjoint=True)
# [[26, 30], [38, 44]]  <- A^T B, correct

LinearOperator.solve passes self.adjoint() as left_operator to self._linop_solve. For an inversion that is a LinearOperatorAdjoint, and LinearOperatorInversion._linop_solve calls left_operator.operator.matmul(...), where .operator is the inversion itself. This seems to have come in when the solve registry was replaced with _linop_solve methods (9c0db34).

The fix only takes the shortcut when left_operator is a LinearOperatorInversion and otherwise falls back to the generic LinearOperator._linop_solve (same kind of guard as LinearOperatorLowerTriangular._linop_matmul).

The new test_solve_operator_matches_solve_tensor compares operator and tensor solve for all adjoint/adjoint_arg combinations. It fails before the fix (adjoint=True) and passes after, and the rest of linear_operator_inversion_test.py still passes. I ran it against tf-nightly 2.23.0.dev20261101 with the patched module, not a source build.

I used Claude Code to write this change.

`LinearOperator.solve(rhs_operator, adjoint=True)` passes `self.adjoint()`
as `left_operator` to `self._linop_solve`. For `LinearOperatorInversion`
that is a `LinearOperatorAdjoint`, whose `.operator` is the inversion
itself, so `_linop_solve` returned `A^-1 B` instead of `A^H B`.

Only take the shortcut when `left_operator` is a `LinearOperatorInversion`
and fall back to the generic solve otherwise.
@google-ml-butler google-ml-butler Bot added the size:S CL Change Size: Small label Nov 2, 2026
@google-ml-butler
google-ml-butler Bot requested a review from cantonios November 2, 2026 13:43
@google-ml-butler google-ml-butler Bot added the awaiting review Pull request awaiting review label Nov 2, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request adds a test case to verify that solving with a LinearOperator as the right-hand side matches solving with a dense tensor, and updates _linop_solve in LinearOperatorInversion to handle cases where left_operator is not a direct instance of LinearOperatorInversion (e.g., when adjoint=True). The reviewer suggested optimizing the fallback logic in _linop_solve when left_operator is an adjoint of LinearOperatorInversion to avoid creating an inefficient, nested LinearOperatorComposition.

Comment on lines +190 to +194
# With `adjoint=True`, `left_operator` is `self.adjoint()`, which is not a
# `LinearOperatorInversion`.
if isinstance(left_operator, LinearOperatorInversion):
return left_operator.operator.matmul(right_operator)
return super()._linop_solve(left_operator, right_operator)

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.

high

When adjoint=True, left_operator is a LinearOperatorAdjoint wrapping a LinearOperatorInversion. Falling back to super()._linop_solve in this case results in a generic fallback that creates a nested LinearOperatorComposition containing LinearOperatorInversion(LinearOperatorAdjoint(LinearOperatorInversion(A))). This is highly inefficient and computationally expensive compared to directly performing the adjoint matrix multiplication.\n\nWe can optimize this by checking if the adjoint of left_operator is an instance of LinearOperatorInversion, and if so, directly returning the adjoint of the underlying operator multiplied by right_operator (i.e., A^H . B).

Suggested change
# With `adjoint=True`, `left_operator` is `self.adjoint()`, which is not a
# `LinearOperatorInversion`.
if isinstance(left_operator, LinearOperatorInversion):
return left_operator.operator.matmul(right_operator)
return super()._linop_solve(left_operator, right_operator)
# With `adjoint=True`, `left_operator` is `self.adjoint()`, which is not a\n # `LinearOperatorInversion`.\n if isinstance(left_operator, LinearOperatorInversion):\n return left_operator.operator.matmul(right_operator)\n if isinstance(left_operator.adjoint(), LinearOperatorInversion):\n return left_operator.adjoint().operator.adjoint().matmul(right_operator)\n return super()._linop_solve(left_operator, right_operator)
References
  1. Identify unnecessary memory usage or redundant computations to ensure performance and efficiency. (link)

@nithyak0204 nithyak0204 self-assigned this Nov 3, 2026
@nithyak0204
nithyak0204 requested a review from a team November 3, 2026 04:31
@nithyak0204 nithyak0204 added the python Pull requests that update Python code label Nov 3, 2026
@github-project-automation github-project-automation Bot moved this to Assigned Reviewer in PR Queue Nov 3, 2026
dmiltr3
dmiltr3 previously approved these changes Nov 3, 2026

@dmiltr3 dmiltr3 left a comment

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.

In tensorflow/python/ops/linalg/linear_operator_inversion.py:190-191, self.adjoint() returns self when self.is_self_adjoint is True. Consider updating the inline comment to note that case:

    # With `adjoint=True`, `left_operator` is `self.adjoint()`, which is a
    # `LinearOperatorAdjoint` unless `self.is_self_adjoint` is True.

@google-ml-butler google-ml-butler Bot added kokoro:force-run Tests on submitted change ready to pull PR ready for merge process labels Nov 3, 2026
@github-project-automation github-project-automation Bot moved this from Assigned Reviewer to Approved by Reviewer in PR Queue Nov 3, 2026
@b84330808

Copy link
Copy Markdown
Contributor Author

Thanks! Updated the comment in e5831f7.

@google-ml-butler google-ml-butler Bot removed the ready to pull PR ready for merge process label Nov 3, 2026
@google-ml-butler google-ml-butler Bot added the ready to pull PR ready for merge process label Nov 3, 2026
@nithyak0204 nithyak0204 removed the awaiting review Pull request awaiting review label Nov 4, 2026
@copybara-service
copybara-service Bot merged commit a079ae4 into tensorflow:master Nov 9, 2026
17 checks passed
@github-project-automation github-project-automation Bot moved this from Approved by Reviewer to Merged in PR Queue Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kokoro:force-run Tests on submitted change python Pull requests that update Python code ready to pull PR ready for merge process size:S CL Change Size: Small

Projects

Status: Merged

Development

Successfully merging this pull request may close these issues.

3 participants