Fix backprop through cot_laplacian

Summary:
`_cot_laplacian_python` used in-place tensor operations (`clamp_`, `/=`,
`+=`) on intermediates that participate in autograd. Once the resulting
sparse Laplacian is used in a backward pass, these in-place mutations
raise a runtime error:

```
RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation: [torch.sparse.FloatTensor [4, 4]], which is output 0 of SparseCooTensorWithDimsAndTensors, is at version 1; expected version 0 instead. Hint: enable anomaly detection to find the operation that failed to compute its gradient, with torch.autograd.set_detect_anomaly(True, check_nan=False).
```

Replace the in-place ops with out-of-place equivalents so gradients can
flow back to the input vertices.

- `clamp_` → `clamp`
- `cot /= 4.0` → `cot = cot / 4.0`
- `L += L.t()` → `L = L + L.t()`

Reviewed By: bottler

Differential Revision: D111896032

fbshipit-source-id: 4c36677487bae5d37a9d81cb791400576f4b2c5f
This commit is contained in:
Roman Shapovalov
2026-07-14 12:07:08 -07:00
committed by meta-codesync[bot]
parent c8fcd83ff9
commit 3b5ab51de5
2 changed files with 15 additions and 4 deletions

View File

@@ -103,8 +103,8 @@ def cot_laplacian(
s = 0.5 * (A + B + C)
# note that the area can be negative (close to 0) causing nans after sqrt()
# we clip it to a small positive value
# pyre-fixme[16]: `float` has no attribute `clamp_`.
area = (s * (s - A) * (s - B) * (s - C)).clamp_(min=eps).sqrt()
# pyre-fixme[16]: `float` has no attribute `clamp`.
area = (s * (s - A) * (s - B) * (s - C)).clamp(min=eps).sqrt()
# Compute cotangents of angles, of shape (sum(F_n), 3)
A2, B2, C2 = A * A, B * B, C * C
@@ -112,7 +112,7 @@ def cot_laplacian(
cotb = (A2 + C2 - B2) / area
cotc = (A2 + B2 - C2) / area
cot = torch.stack([cota, cotb, cotc], dim=1)
cot /= 4.0
cot = cot / 4.0
# Construct a sparse matrix by basically doing:
# L[v1, v2] = cota
@@ -127,7 +127,7 @@ def cot_laplacian(
# L[v2, v1] = cota
# L[v0, v2] = cotb
# L[v1, v0] = cotc
L += L.t()
L = L + L.t()
# For each vertex, compute the sum of areas for triangles containing it.
idx = faces.view(-1)

View File

@@ -118,3 +118,14 @@ class TestLaplacianMatrices(TestCaseMixin, unittest.TestCase):
Lnaive[e1, e0] += w01
self.assertClose(L.to_dense(), Lnaive)
def test_cot_laplacian_backward(self):
"""Regression: in-place ops in _cot_laplacian_python break autograd."""
verts = torch.tensor(
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0]],
requires_grad=True,
)
faces = torch.tensor([[0, 1, 2], [1, 3, 2]])
L, inv_areas = cot_laplacian(verts, faces)
(L.to_dense().sum() + inv_areas.sum()).backward()
assert verts.grad is not None