Repository navigation
Keep float scalars exact in float64 ops in ScalarToTensorPass - #24168
Conversation
A float64 program with x * 0.1 + 0.3 then / 0.7 must give the same result as eager after ScalarToTensorPass and RemoveMixedTypeOperators. Today the scalars are rounded to float32 first. See pytorch#24167.
torch.tensor(value) makes a float32 constant, so a float scalar used with a float64 tensor was rounded to float32 before RemoveMixedTypeOperators cast it back to float64. When the op has a float64 tensor operand, create the constant as float64. Other dtypes are unchanged. Fixes pytorch#24167
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24168
Note: Links to docs will display an error until the docs builds have been completed. ⏳ No Failures, 1 PendingAs of commit 2eac08c with merge base 5b14a2f ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
| if isinstance(value, float) and has_float64_tensor | ||
| else None | ||
| ) | ||
| return torch.tensor(value, dtype=dtype) |
There was a problem hiding this comment.
Float scalars in python are always float64 no? Is the better fix just to always cast them to float64 tensors regardless of if there is another in the op
There was a problem hiding this comment.
Python floats are float64, but the scalar's dtype decides the result dtype when the other operand is an integer tensor. With the scalar as a 0-dim float64 tensor, int32_tensor * 0.1 returns float64, while eager (and this PR) return float32. I tried always using float64 on x * 0.1, x + 0.1, 0.1 - x and x / 0.7, lowered with to_edge(...).to_executorch() and run through portable:
| input | always float64 | this PR |
|---|---|---|
| float64 | exact | exact |
| float32, float16 | unchanged | unchanged |
| int32 | float64 output, eager is float32 | float32, matches eager |
So I'd keep float64 only when the op already has a float64 tensor operand. The other way would be to pick the dtype from meta["val"] of the node's output, if you'd prefer that.
Summary
Fixes #24167.
ScalarToTensorPassturns a Python scalar operand intotorch.tensor(value), which is float32 for a float. In a float64 program,RemoveMixedTypeOperatorsthen casts that constant to float64, but the value has already been rounded, sox * 0.1multiplies by0.10000000149011612.x + 0.1,x - 0.1,x / 0.1,0.1 - xandtorch.deg2rad(x)were off by about 1e-9 to 1.5e-8 relative.When the op has a float64 tensor operand, the pass now creates float scalars as float64 constants. Those graphs also lose the
_to_dim_order_copythat cast the constant. Float32, float16 and integer graphs get the same constants as before.Test plan
test_scalar_to_tensor_pass_keeps_float64_scalarsinexir/tests/test_passes.pyruns(x * 0.1 + 0.3) / 0.7on a float64 input throughScalarToTensorPassandRemoveMixedTypeOperatorsand checks the result equals eager exactly. It fails before this change and passes after.I ran it against executorch 1.5.1 and torch 2.14.1+cpu on Linux x86_64 (WSL2), with this file swapped in (the pass is the same on main). Of the 37 tests in
test_passes.pymatchingscalar,mixed,constant_prop,to_copy,debug_handle,symintordim_order, the same ones pass before and after apart from the new test. Five fail on 1.5.1 both with and without this change, because main's test file needs newerconstant_prop_passand memory-format code than that release has. Loweringx * 0.1,x + 0.1,x / 0.1,0.1 - xandtorch.deg2rad(x)withto_edge(...).to_executorch()and running them through the portable pybindings now matches eager for float64, and gives the same graph and output as before for float32, float16, and int32 inputs.ufmt(black 24.4.2, usort 1.0.8.post1) andflake8pass on the changed files.This PR was authored with AI assistance (Claude Code).
cc @nil-is-all @JacobSzwejbka @angelayi