Skip to content
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 crash when casting decimals to long #6103

Merged
merged 7 commits into from
Aug 1, 2022

Conversation

razajafri
Copy link
Collaborator

@razajafri razajafri commented Jul 26, 2022

This PR avoids type mismatch that can occur if the min/max decimal values are a different precision than the column being casted in ANSI mode.

e.g.
Casting 222.22 to long will result in a call to check to see the values are in range before converting them to long values. That check will cause a crash as the minimum value possible in decimal(5,2) will result in a Decimal 128 value and the assertValuesInRange method in GpuCast will result in a type mismatch error from cudf when it calls lessThan.

@ttnghia Suggested a way around this is to first find the minimum value in the input column and compare that value to the decimal 128 value using a Java BigDecimal.compareTo which can handle comparing Decimals of different precisions.

fixes #6128

Signed-off-by: Raza Jafri <rjafri@nvidia.com>
@razajafri razajafri self-assigned this Jul 26, 2022
@sameerz sameerz added the bug Something isn't working label Jul 27, 2022
Comment on lines 385 to 386
withResource(input.min()) { min =>
withResource(input.max()) { max =>
Copy link
Collaborator

Choose a reason for hiding this comment

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

min or max should be just a number, right? So we won't need to wrap them.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not just a number, it is a Scalar object (https://github.com/rapidsai/cudf/blob/branch-22.08/java/src/main/java/ai/rapids/cudf/Scalar.java#L35), so it has to be closed.

Copy link
Collaborator

@ttnghia ttnghia Jul 28, 2022

Choose a reason for hiding this comment

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

If it is scalar then we may want to call isValid to check the min and max values. Otherwise, if the input is all nulls, these values will be invalid.

.setScale(dt.scale, BigDecimal.RoundingMode.DOWN).bigDecimal
assertValuesInRange(input, Scalar.fromDecimal(min), Scalar.fromDecimal(max))
withResource(input.min()) { min =>
Copy link
Collaborator

Choose a reason for hiding this comment

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

add comment about why we are doing this

Copy link
Collaborator

Choose a reason for hiding this comment

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

In fact, the assertValuesInRange function is inefficient: It calls less operator then any then greater then any, and all these operations are O(N). We can achieve the result by half of computation by using this new approach: min then max in O(N) then compare the min/max values with the boundary in O(1).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've filed a corresponding issue: #6130

@tgravescs
Copy link
Collaborator

it would be nice to have an issue associated with this that describes the problem and reproducing it

withResource(input.max()) { max =>
if (min.getBigDecimal().compareTo(bigDecimalMin) == -1 ||
max.getBigDecimal().compareTo(bigDecimalMax) == 1) {
throw new IllegalStateException(GpuCast.INVALID_INPUT_MESSAGE)
Copy link
Collaborator

Choose a reason for hiding this comment

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

this same thing throws in Spark on Cpu?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we need to add an integration test to compare?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this same thing throws in Spark on Cpu?

CPU throws an ArithmeticException but I wanted to match the existing behavior. I have changed it to match the CPU.

Maybe we need to add an integration test to compare?

Just adding an integration test to for a handful of values or testing a wider range? I experimented with this and it will be a lot more involved test as there are many other types of exceptions that can be thrown in ANSI e.g. overflow

Copy link
Collaborator

Choose a reason for hiding this comment

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

I can imagine that this will be the case for other cast operations.
If we do not have integration tests to cover the behavior of CPU vs GPU, then we can create a new followup issue to improve the tests including the "Decimal to Long".

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 for adding a test to make sure that we are not regressing on the exception.

withResource(input.max()) { max =>
if (min.getBigDecimal().compareTo(bigDecimalMin) == -1 ||
max.getBigDecimal().compareTo(bigDecimalMax) == 1) {
throw new IllegalStateException(GpuCast.INVALID_INPUT_MESSAGE)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we need to add an integration test to compare?

generateValidValuesDecimalDF(Short.MinValue, Short.MaxValue, 18, 3), sparkConf) {
frame => testCastTo(DataTypes.LongType)(frame)
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it possible to test that we actually throw the expected exception?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This test is only for the valid values. May be I need to add a test that tests just that an exception is thrown?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We won't need to have a new case as you are throwing the same exception because this will be covered in testCastFailsForBadInputs("ansi_cast overflow decimals to longs",..).
If we change the behavior to match the CPU, then we would need a new test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK, then that test is already testing the exception that is thrown.

Signed-off-by: Raza Jafri <rjafri@nvidia.com>
Signed-off-by: Raza Jafri <rjafri@nvidia.com>
tgravescs
tgravescs previously approved these changes Jul 28, 2022
Comment on lines 381 to 383
val bigDecimalMin = BigDecimal(Long.MinValue)
.setScale(dt.scale, BigDecimal.RoundingMode.DOWN).bigDecimal
val max = BigDecimal(Long.MaxValue)
val bigDecimalMax = BigDecimal(Long.MaxValue)
Copy link
Collaborator

Choose a reason for hiding this comment

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

bigDecimalMax/Min are not minimums of BigDecimal, the names are confusing. The conversion from long is probably non-trivial, might be worthwhile to make these class vals

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The BigDecimal will have to be re-scaled and most of the calculation is done in that method. I am not sure what we will gain by just making a BigDecimal(Long.MIN) as a class val. I can still do it but I just wanted to make sure I was understanding you correctly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator

@gerashegalov gerashegalov Jul 29, 2022

Choose a reason for hiding this comment

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

Note BigDecimal instances are just like Integer immutable. So setScale will produce another object generated from the objects we can cache such as BigDecimal(Long.MaxValue)

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is a valid argument depending on how frequent casting Decimals to Longs is done in a workload. The other concern is using static/constants increases the footprint of the VM and slows down the initialization.
Anyway, if this is going to be addressed within the same PR, then I suggest to create constants in a util to be used in different classes.
The repo has two other locations that use constants BigDecimal.
in arithemtic.scala there are two local variables

 val zero = BigDecimal(0).bigDecimal

Then we can create three constants BigDecimal(0).bigDecimal, BigDecimal(Long.MaxValue), and BigDecimal(Long.MinValue)

All other constant BigDecimals are in test classes which we can ignore.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can we please handle creating of the util class in a separate PR?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree it is fair. Both BigDecimal(Long.MaxValue), and BigDecimal(Long.MinValue) were not introduced in this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we don't have yet to worry about the constant pool because of the two constants being added. We can do a more sweeping refactoring in a separate PR.

Signed-off-by: Raza Jafri <rjafri@nvidia.com>
Signed-off-by: Raza Jafri <rjafri@nvidia.com>
@razajafri
Copy link
Collaborator Author

build

Signed-off-by: Raza Jafri <rjafri@nvidia.com>
gerashegalov
gerashegalov previously approved these changes Jul 29, 2022
Copy link
Collaborator

@gerashegalov gerashegalov left a comment

Choose a reason for hiding this comment

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

amahussein
amahussein previously approved these changes Jul 29, 2022
Copy link
Collaborator

@amahussein amahussein left a comment

Choose a reason for hiding this comment

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

LGTM.

A minor styling issue is to have constants in capital letters.
I am not sure that we have a clear formatting rule for this, but it looks like all the constants in GpuCast in capital letters.

@@ -177,6 +177,8 @@ object GpuCast extends Arm {
private val TIMESTAMP_TRUNCATE_REGEX = "^([0-9]{4}-[0-9]{2}-[0-9]{2} " +
"[0-9]{2}:[0-9]{2}:[0-9]{2})" +
"(.[1-9]*(?:0)?[1-9]+)?(.0*[1-9]+)?(?:.0*)?$"
private val bigDecimalLongMin = BigDecimal(Long.MinValue)
private val bigDecimalLongMax = BigDecimal(Long.MaxValue)
Copy link
Collaborator

Choose a reason for hiding this comment

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

private val BIG_DECIMAL_LONG_MIN = BigDecimal(Long.MinValue)
private val BIG_DECIMAL_LONG_MAX = BigDecimal(Long.MaxValue)

val max = BigDecimal(Long.MaxValue)
.setScale(dt.scale, BigDecimal.RoundingMode.DOWN).bigDecimal
val min = bigDecimalLongMin.setScale(dt.scale, BigDecimal.RoundingMode.DOWN).bigDecimal
val max = bigDecimalLongMax.setScale(dt.scale, BigDecimal.RoundingMode.DOWN).bigDecimal
Copy link
Collaborator

Choose a reason for hiding this comment

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

        val min = BIG_DECIMAL_LONG_MIN.setScale(dt.scale, BigDecimal.RoundingMode.DOWN).bigDecimal
        val max = BIG_DECIMAL_LONG_MAX.setScale(dt.scale, BigDecimal.RoundingMode.DOWN).bigDecimal

@razajafri
Copy link
Collaborator Author

build

Signed-off-by: Raza Jafri <rjafri@nvidia.com>
@razajafri razajafri dismissed stale reviews from amahussein and gerashegalov via a267a7d July 29, 2022 18:54
Copy link
Collaborator

@amahussein amahussein left a comment

Choose a reason for hiding this comment

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

Thanks Raza!

LGTM.

@sameerz
Copy link
Collaborator

sameerz commented Aug 1, 2022

build

@razajafri razajafri merged commit e77b40f into NVIDIA:branch-22.08 Aug 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG] Can not ansi cast decimal type to long type while fetching decimal column from data table
8 participants