Repository navigation
fix(geometry): preserve quaternion rotation matrix dtype - #3982
Conversation
Generated-by: OpenAI Codex (GPT-5)
cebc6dd to
4c0735f
Compare
ducha-aiki
left a comment
There was a problem hiding this comment.
Correct fix, and the diff is smaller than the defect it closes. Verified on current main (35555b55), not on your branch — see the rebase note.
Verified
main with this PR
float16 (4,) -> torch.float32 -> torch.float16
bfloat16 (4,) -> torch.float32 -> torch.bfloat16
float32 (4,) -> torch.float32 -> torch.float32
float64 (4,) -> torch.float64 -> torch.float64
float16 (1, 4) -> torch.float16 -> torch.float16 (already correct, unchanged)
float32 and float64 outputs are bitwise identical before and after — I compared the raw words via .view(torch.int32) / .view(torch.int64) on a batch of five random unit quaternions and got the same integers on both trees. Replacing a 0-dim torch.tensor(1.0) with a Python 1.0 turns a participant in type promotion into a wrapped scalar, which is exactly the right lever: it cannot move a result, only a dtype.
grep -rnE "#3954|issues/3954" kornia/ after your change: no hits.
Your change fixes 16 half-precision test failures you did not claim
tests/geometry --dtype=float16,bfloat16 on current main: 696 failures. With your diff applied: 680, and the diff of the two ID sets is 16 removals and zero additions:
TestQuaternionToRotationMatrix::test_unit_quaternion [float16, bfloat16]
TestQuaternionToRotationMatrix::test_x_rotation [float16, bfloat16]
TestQuaternionToRotationMatrix::test_y_rotation [float16, bfloat16]
TestQuaternionToRotationMatrix::test_z_rotation [float16, bfloat16]
TestRotationMatrixToQuaternion::test_back_and_forth [float16, bfloat16]
TestRotationMatrixToQuaternion::test_cond1_180_rot_x [float16, bfloat16]
TestRotationMatrixToQuaternion::test_cond2_180_rot_y [float16, bfloat16]
tests/geometry/test_quaternion.py::TestQuaternion::test_matrix [float16, bfloat16]
Every one of those is an ordinary test comparing against a half-precision expected tensor, failing purely because the actual came back float32. This is worth putting in the PR body — "closes #3954 and repairs 16 pre-existing half-precision failures, with no new ones" is a much stronger statement than the one you made, and it is the kind of number #4033 (the half-precision CI job) will want on record.
At float32 the suite is unchanged: tests/geometry gives 0 failures with your diff, same as main.
One request: keep the issue number on the guard
You replaced test_convention_output_dtype_equals_input_dtype_3954 with test_unbatched_preserves_low_precision_dtype and it is a genuine pin — it fails on unpatched main for both half dtypes, and adding the value assertion against eye(3) is an improvement over the dtype-only original. Deleting _undo_3954_upcast and its four call sites was also exactly right; the helper's own comment asked for it.
But after your change grep -rnE "#3954" tests/ returns nothing. Per AGENTS.md a pin goes on naming its issue after the fix lands, and the convention-block program greps by both the #NNNN anchor and the test_convention_* prefix. As it stands, nothing connects the surviving guard to the closed issue, and the mechanism — 0-dim tensors losing to dimensioned ones in type promotion, so only the unbatched shape was affected — is now recorded nowhere. That mechanism is the non-obvious part; it is why (4,) broke and (1, 4) did not.
The minimal change I would like: keep the name test_convention_output_dtype_equals_input_dtype_3954 (just drop the xfail marker rather than deleting the test), fold your assert_close against eye(3) into it, and keep a two-line version of the promotion explanation. That is fewer edits than what you have and it preserves both greps.
Rebase
Base is d73bf9c0, so this branch has not run the pins from #4006 or #4028; CI dates to 2026-08-25. Everything above was measured with your diff applied to current main, so I know it is clean — please rebase so your own CI shows it.
Approving on the verification. Restore the _3954 name on the guard and this is ready.
Posted on behalf of @ducha-aiki by Claude (Opus 5).
The replacement guard was correct but unnamed: after the rename `grep -rnE "kornia#3954" tests/` returned nothing, so nothing connected the surviving pin to the closed issue, and the mechanism -- 0-dim tensors losing to dimensioned ones in type promotion, which is why (4,) broke and (1, 4) did not -- was recorded nowhere. Restores the test_convention_*_3954 name the convention program greps for, keeps the author's value assertion against eye(3), and adds back the explicit dtype skip so a backend without float16/bfloat16 reports as skipped rather than failing on a RuntimeError. The batched shape is carried as a third assertion so a fix that special-cased one shape would not satisfy the pin. Still fails 2/2 on the unfixed library, passes 2/2 with the fix. Signed-off-by: Dmytro Mishkin <[email protected]> Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01X2RcRuN1AoKWwD8n1hVSjq
ducha-aiki
left a comment
There was a problem hiding this comment.
Pushed the naming change I asked for rather than leaving you a round-trip — head is 25eee7d0, one commit on top of yours, no rebase needed.
Your replacement guard was correct; it was only unnamed. After the rename grep -rnE "#3954" tests/ returned nothing, so nothing linked the surviving pin to the closed issue and the mechanism was recorded nowhere. The commit restores the test_convention_output_dtype_equals_input_dtype_3954 name the convention program greps for and writes the mechanism down: 0-dim tensors lose to dimensioned ones in type promotion, which is exactly why (4,) broke and (1, 4) did not.
Three small additions while I was there:
- Your
assert_closeagainsteye(3)is kept — it is an improvement on the dtype-only original. _skip_if_dtype_unavailable(device, half_dtype)is back, so a backend without float16/bfloat16 reports as skipped instead of failing on aRuntimeError.- The batched shape is carried as a third assertion, so a fix that special-cased one shape would not satisfy the pin.
Still 2 failed on the pre-fix library and 2 passed with your change. tests/geometry 2263 passed / 0 failed on the pushed head; ruff clean.
Re-approving; the earlier approval was dismissed by my push. Worth restating what this PR actually buys, because your description undersells it: torch.tensor(1.0) -> 1.0 leaves float32/float64 bitwise identical and repairs 16 pre-existing half-precision failures with zero new ones.
Posted on behalf of @ducha-aiki by Claude (Opus 5).
ducha-aiki
left a comment
There was a problem hiding this comment.
Final verification round, 25eee7d0 merged onto current main (the PR base was 9 commits behind).
grep -rnE "#3954|issues/3954" kornia/ docs/is empty; the two remaining hits are inside the restored pin itself._undo_3954_upcasthas no callers left anywhere in the tree.tests/geometry/test_conversions.py --dtype=float32,float64: 602 passed, 20 xfailed. Doctests pass; ruff clean.- The restored
test_convention_output_dtype_equals_input_dtype_3954keeps the name, so the_3954anchor the AGENTS.md grep protocol relies on survives.
Worth recording that this does more than close #3954 — it clears seven pre-existing bfloat16 failures and introduces none. Diffing the failure sets of tests/geometry/test_conversions.py --dtype=bfloat16 -k "RotationMatrix or Euler":
fixed by this PR (fail on main, pass here):
TestQuaternionToRotationMatrix::test_unit_quaternion
TestQuaternionToRotationMatrix::test_x_rotation
TestQuaternionToRotationMatrix::test_y_rotation
TestQuaternionToRotationMatrix::test_z_rotation
TestRotationMatrixToQuaternion::test_back_and_forth
TestRotationMatrixToQuaternion::test_cond1_180_rot_x
TestRotationMatrixToQuaternion::test_cond2_180_rot_y
new failures introduced: none
The seven that still fail at bfloat16 fail identically on main and are a separate tolerance question.
Approving. This is still marked draft — it needs gh pr ready 3982 before it can be merged.
Posted on behalf of @ducha-aiki by Claude (Opus 5).
There was a problem hiding this comment.
Pull request overview
Fixes a dtype-promotion bug in kornia.geometry.conversions.quaternion_to_rotation_matrix where unbatched (4,) float16/bfloat16 inputs were silently upcast to float32, and updates the regression coverage while removing now-obsolete #3954 scaffolding.
Changes:
- Replace the internal
one = torch.tensor(1.0)literal with a Python scalar to avoid dtype upcasting for unbatched half-precision quaternions. - Add a focused regression test that asserts dtype preservation (and value oracle) for unbatched
float16/bfloat16inputs, and validate the batched case in the same test. - Remove the temporary
_undo_3954_upcasthelper and retire the prior #3954 strict-xfail/wart pin machinery.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
kornia/geometry/conversions.py |
Changes the one literal in quaternion_to_rotation_matrix to preserve input dtype for unbatched half-precision quaternions. |
tests/geometry/test_conversions.py |
Replaces the old #3954 scaffolding with a direct regression asserting dtype preservation and expected identity output. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # components of an unbatched (4,) input tied with it and float32 won the category. The fix | ||
| # makes `one` a Python float, which is a wrapped scalar and does not participate in | ||
| # promotion at all. That mechanism is why this pin is UNBATCHED -- the batched shape passed | ||
| # both before and after and cannot detect the defect. It is carried here as the third | ||
| # assertion anyway, so a fix that special-cased one shape would not satisfy this pin. |
Which lane? (check one — see Contributing Guide)
help wantedissue / benchmark results. No prior issue needed.Fixes/Relates to: Fixes #3954
What does this PR do?
For an unbatched
(4,)quaternion, each component is a 0-dimensional tensor. The existing 0-dimensional CPUfloat32tensor used for the matrix diagonal therefore ties with afloat16orbfloat16component during type promotion and lifts the result tofloat32.This replaces that tensor with a Python scalar. PyTorch's wrapped-scalar promotion preserves the input tensor's dtype without allocating a separate device tensor. A focused regression covers both affected low-precision dtypes and uses the identity rotation matrix as its value oracle.
This branch is rebased onto current
main(d73bf9c0). Merged PR #3967 added temporary #3954 documentation, a strict xfail/wart pair, and_undo_3954_upcastcasts. The fix makes the xfail XPASS and the wart fail for both dtypes, so this PR also retires only that now-obsolete #3954 scaffolding. All unrelated convention and wart coverage remains and now checks the function output directly.Test log
Environment: macOS arm64, Python 3.13.5, PyTorch 2.13.0, CPU.
Current
mainbaseline:With the functional fix applied before cleanup, the two strict-xfail cells become
XPASS(strict)failures and both wart cells fail because the output is nowfloat16/bfloat16. This execution proves those two temporary pins are obsolete rather than merely stale-looking.Current rebased head:
The listed xfails are unrelated current-main convention pins; the #3954 strict xfail and wart were removed only after the execution proof above.
AI Usage Disclosure (pick the closest — the 🟡/🔴 line is fuzzy and only deception is sanctioned; none of these is a bad answer, see AI_POLICY.md)
Generated-By: OpenAI Codex (GPT-5). This remains a Draft pending personal human line-by-line review before readiness.