Skip to content

fix(geometry): preserve quaternion rotation matrix dtype - #3982

Merged
ducha-aiki merged 2 commits into
kornia:mainfrom
fallenmi:agent/preserve-quaternion-matrix-dtype
Aug 27, 2026
Merged

ducha-aiki merged 2 commits into
kornia:mainfrom
fallenmi:agent/preserve-quaternion-matrix-dtype

Conversation

@fallenmi

@fallenmi fallenmi commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Which lane? (check one — see Contributing Guide)

  • 🟢 Green lane — test-first bug fix / verified docs fix / help wanted issue / benchmark results. No prior issue needed.
  • 🟠 Discuss-first — feature or behavior change; a maintainer confirmed the scope on the linked issue.

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 CPU float32 tensor used for the matrix diagonal therefore ties with a float16 or bfloat16 component during type promotion and lifts the result to float32.

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_upcast casts. 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 main baseline:

$ pytest -q \
    ...::test_convention_output_dtype_equals_input_dtype_3954 \
    ...::test_wart_half_precision_input_is_upcast_to_float32_3954 \
    --device=cpu
2 passed, 2 xfailed in 0.43s

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 now float16/bfloat16. This execution proves those two temporary pins are obsolete rather than merely stale-looking.

Current rebased head:

$ pytest -q \
    tests/geometry/test_conversions.py::TestQuaternionToRotationMatrix::test_unbatched_preserves_low_precision_dtype \
    --device=cpu
2 passed in 0.33s

$ KORNIA_TEST_DTYPE=float16,bfloat16 pytest -q \
    tests/geometry/test_conversions.py::TestQuaternionToRotationMatrix \
    --device=cpu
34 passed, 4 skipped, 1 xfailed in 0.35s

$ KORNIA_TEST_DTYPE=float16,bfloat16 pytest -q \
    tests/geometry/test_conversions.py::TestAngleAxisToRotationMatrix::test_convention_positive_angle_about_z_maps_x_to_y \
    tests/geometry/test_conversions.py::TestQuaternionFromEuler::test_convention_composition_is_rz_ry_rx \
    --device=cpu
4 passed in 0.27s

$ pytest -q tests/geometry/test_conversions.py tests/integration/test_conversions.py --device=cpu
487 passed, 1 skipped, 24 xfailed in 1.51s

$ ruff check kornia/geometry/conversions.py tests/geometry/test_conversions.py
All checks passed!

$ ruff format --check kornia/geometry/conversions.py tests/geometry/test_conversions.py
2 files already formatted

$ uvx --from 'ty==0.0.9' ty check kornia
All checks passed!

$ git diff --check origin/main...HEAD
# no output

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)

  • 🟢 Human-written
  • 🟡 AI-assisted — AI helped; I reviewed the resulting change and ran the relevant tests
  • 🔴 AI-generated — an agent wrote most of the change; automated verification is documented above, and personal human review is still pending

Generated-By: OpenAI Codex (GPT-5). This remains a Draft pending personal human line-by-line review before readiness.

@fallenmi
fallenmi force-pushed the agent/preserve-quaternion-matrix-dtype branch from cebc6dd to 4c0735f Compare August 25, 2026 14:27
ducha-aiki
ducha-aiki previously approved these changes Aug 27, 2026

@ducha-aiki ducha-aiki left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 ducha-aiki left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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_close against eye(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 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 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 ducha-aiki left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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_upcast has 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_3954 keeps the name, so the _3954 anchor 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).

@ducha-aiki
ducha-aiki marked this pull request as ready for review August 27, 2026 20:19
Copilot AI lite review requested due to automatic review settings August 27, 2026 20:19
@ducha-aiki
ducha-aiki merged commit 69b3077 into kornia:main Aug 27, 2026
37 of 42 checks passed

Copilot AI 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.

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/bfloat16 inputs, and validate the batched case in the same test.
  • Remove the temporary _undo_3954_upcast helper 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.

Comment on lines +1296 to +1300
# 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

quaternion_to_rotation_matrix silently upcasts float16/bfloat16 to float32 (hard-coded torch.tensor(1.0))

3 participants