-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix bounds inference for uint -> int casts #7814
base: main
Are you sure you want to change the base?
Conversation
Testing this in Google, I get many many failures of the form |
The question now is whether to roll back us defining uint -> int casts as having wrapping semantics, or to patch those pipelines. I'd like to slowly reduce the number of types of UB we have over time, so if patching seems tractable (e.g. by inserting a max with zero around the index), that would be best. |
I'll have to figure out how many pipelines it is. (there are lots of failures, but maybe it's just a handful of shared pipelines.) What is the likely failure pattern I'm looking for? |
Access to an input named "foo" using a computed uint32 index. This is a bit of a guess but I think the needed patch is something like: Before:
After:
|
The first case I'm looking at doesn't have anything obviously of that form, but there are a lot of helper functions. I wonder if there is a bottleneck for trapping most/all int->uint cases that would help? |
Running the failure case with HL_DEBUG_CODEGEN=4 reveals something possibly interesting: just before the failure, I see
undefined? |
That undefined is fine. It means the simplifier has determined the new let is not necessary to insert. |
The immediate failure looks like this:
|
I pushed a temporary change that should give a more useful error |
The failure is coming from INSIDE HALIDE! </> Look at
(Spoiler alert: (In hindsight, I really wish that Halide had adopted the Golang property of never doing implicit type conversions...) |
Unfortunately, if I change the bad line to |
Can repro! I'll investigate. |
Possible fix pushed |
OK, that fixes the unbounded access... now I get failures with |
Yeah, this is another one where it is gonna be tricky to find these errors in existing code without some helpers. |
I pushed a user_warning for when bounds inference does something different to what it used to do. |
The first one I found basically amounted to code that used explicit casting to do a 'widening' mul (this code predates the
switching it to use |
I'm a little alarmed that widening_mul heals that, because any 16-bit -> i32 cast should be bounded, because i32 can represent i16 and u16. Also, widening_mul just means the same thing. |
The evidence suggests otherwise? Not sure I can give you an easy repro case from it but maybe using that snippet and seeing how the calculated bounds differ would be useful. |
I'll apply that and retry. |
Update: maybe that's because the Call visitor in Bounds calculation doesn't have a case for If I add a specialization that is similar to
..then using |
Also interesting (though maybe not 100% relevant here): we do specialize for shift_left, but only for signed integers, not for unsigned... in the case of this code, we have a u32 << u32 case where LHS is in 0..65535 and RHS is in 0..16, so bounds should be 0..0xFFFF0000... but since we don't handle it, bounds is neg_inf..pos_inf |
This addresses many (but not all) of the `signed integer overflow` issues we're seeing in Google due to #7814 -- a lot of the issues seems to be in code that uses intrinsics that had no handling in value bounds checking, so the bounds were naively large and overflowed. - Most of the intrinsics from FindIntrinsics.h weren't handled; now they all are (most by lowering to other IR, though the halving_add variants were modeled directly because the bitwise ops don't mesh well) - strict_float() is just a pass-through - round() is a best guess (basically, if bounds exist, expand by one as a worst-case) There are definitely others we should handle here... trunc/floor/ceil probably?
and add one-sided handling for monotonic functions
This is fairly old, where does it stand? |
This one is pending figuring out what breaks inside of google |
It's been a while, do we remember the gist of it |
I think we were knocking cases down one by one, but there are more failures that we have yet to diagnose. |
ok, I will pull it in and see where we stand |
So the fact that |
I see, so people are doing things like my_func(abs(expr)), and that's implicitly my_func(i32(abs(expr))), and the expr could be the most-negative integer if we know nothing about it, in which case that cast now returns the most-negative integer instead of being UB. |
Wait, that can't be the example, because if we know nothing about expr in that case the access is unbounded anyway. I guess there are cases where we know an upper bound on expr but not a lower bound? |
yeah, this is pervasive too, it makes me question whether this is the right design for abs() -- maybe we should change it to return the same type for signed. alternately, add |
in this case the full expr is |
If this really becomes the show-stopper, we could declare that abs of the most negative 32/64-bit integer is UB (which it already is in main if you cast the result back to int due to the signed integer overflow), and then handle int casts of abs specially in bounds inference. |
I don't know if it's a showstopper, but in the first failure I looked at, literally every failure warning was about abs() |
I tried writing a shim like so:
but it doesn't help, e.g.
not sure if this is just a false positive in the sniffing code or not? but I still fail with a buffer being accessed in an unbounded way. |
|
* Handle many more intrinsics in Bounds.cpp This addresses many (but not all) of the `signed integer overflow` issues we're seeing in Google due to #7814 -- a lot of the issues seems to be in code that uses intrinsics that had no handling in value bounds checking, so the bounds were naively large and overflowed. - Most of the intrinsics from FindIntrinsics.h weren't handled; now they all are (most by lowering to other IR, though the halving_add variants were modeled directly because the bitwise ops don't mesh well) - strict_float() is just a pass-through - round() is a best guess (basically, if bounds exist, expand by one as a worst-case) There are definitely others we should handle here... trunc/floor/ceil probably? * Fix round() and strict_float() handling * Update Bounds.cpp * Fixes? * trigger buildbots * Revert saturating_cast handling * Update Bounds.cpp --------- Co-authored-by: Andrew Adams <[email protected]>
* Handle many more intrinsics in Bounds.cpp This addresses many (but not all) of the `signed integer overflow` issues we're seeing in Google due to halide#7814 -- a lot of the issues seems to be in code that uses intrinsics that had no handling in value bounds checking, so the bounds were naively large and overflowed. - Most of the intrinsics from FindIntrinsics.h weren't handled; now they all are (most by lowering to other IR, though the halving_add variants were modeled directly because the bitwise ops don't mesh well) - strict_float() is just a pass-through - round() is a best guess (basically, if bounds exist, expand by one as a worst-case) There are definitely others we should handle here... trunc/floor/ceil probably? * Fix round() and strict_float() handling * Update Bounds.cpp * Fixes? * trigger buildbots * Revert saturating_cast handling * Update Bounds.cpp --------- Co-authored-by: Andrew Adams <[email protected]>
Looking over old outstanding PRs, this one is nearly a year old now -- what's the status on it, do we want to (ever) land it? |
This is technically a bug fix, but it seemed like it caused too many problems in production due to the abs issue. Not sure what to do about it. |
Fixes #7807
Fixes #7810
Note that this may cause previously-functioning pipelines to throw a compile-time error, in cases where they cast a uint32 to an int32 for use in an index expression and were relying on Halide to assume the result is positive. Now that the uint32 -> int32 cast is defined to wrap, the result may not be positive. An example might be the expression:
Previously Halide would have treated this as bounded between 0 and 256. Now it's no longer bounded below, because the uint32 could have been large enough to wrap.
If this causes too much carnage in production pipelines we may have to revert the decision to define wrapping behavior for uint32 -> int32 casts.