[Piglit] [PATCH v3 2/3] arb_shader_precision: add framework for calculating tolerances for complex functions

Ilia Mirkin imirkin at alum.mit.edu
Fri Mar 6 13:12:23 PST 2015


+    def __truediv__(self, other):
+        tol = numpy.float32(2.5)
+        a = self.high / (other.high + _ulpsize(other.high) * tol)
+        b = self.high / (other.low - _ulpsize(other.low) * tol)
+        c = self.low / (other.high + _ulpsize(other.high) * tol)
+        d = self.low / (other.low - _ulpsize(other.low) * tol)

That's not how I understand the tolerance... I understand it as a
"this operation can be off by this much" sort of thing. i.e. it should
be applied *after* the division. Also due to signs you may be moving
things in the wrong direction. You do this sort of thing in your other
ops as well. Also the non-i* operators shouldn't modify self but
rather return a new value.

Lastly I wonder how this can deal with the uncertainty presented by
a*b+c being implemented as 2 separate ops vs one that doesn't do an
intermediate step. Perhaps it should keep around the low/high at
33-bit (or 64-bit for simplicity) precision and use that in
adds/multiplies (and a non-add/multiply operation would "flush" it to
the 32-bit value)


On Fri, Mar 6, 2015 at 3:49 PM, Micah Fedke <micah.fedke at collabora.co.uk> wrote:
> Ilia,
>
> Here is a POC for your interval arithmetic-like method:
>
> http://cgit.collabora.com/git/user/fedke.m/piglit.git/log/?h=complex_tolerances_ia
>
> Only normalize() and length() are implemented right now.
>
> Is that along the lines of what you were thinking?
>
>
> -mf
>
> On 02/28/2015 12:18 PM, Ilia Mirkin wrote:
>>
>> BTW, and this is a bigger concern... your approach appears to
>> basically be along the lines of
>>
>> 1. Perform calculation with ~infinite precision
>> 2. Perform calculation with ... "finite" precision
>> 3. Compare. If diff is too big, don't bother, otherwise keep summing
>> ULP's based on that diff (rather than the allowable error).
>>
>> However this doesn't handle compounding that well. If I have an op
>> that can be off by 1000 ULPs, the computation with low precision won't
>> capture that. Let's say you define an op like 'fma' as a complex op,
>> and the mul part of it can be off by 10 ULPs but the 'add' part of it
>> can't be off by any ULPs. Will the error computed by your "complex"
>> path provide a tolerance of 10 ULPs in that case?
>>
>> I think that the better approach (albeit somewhat complex) would be to
>> define a class like "ImpreciseValue" which would keep both the "low"
>> and "high" values allowed (as np.float32/float64's). Then you can
>> define all operations to compute all 4 possible values, i.e. if it's
>> multiplication, do
>>
>> a.low * b.low
>> a.high * b.low
>> a.low * b.high
>> a.high * b.high
>>
>> And take the min/max of those, and if the multiplication operation has
>> a tolerance of, say, 10 ULP's, spread the min/max apart by that much.
>> Each operation would return one of these ImpreciseValue things, and
>> you should be able to determine an accurate tolerance. (I think the
>> low/high approach works for all the functions we have here... haven't
>> proven it out, but seems like it could be fine, esp over small ranges,
>> since in practice the highest error is with pow and that's only 16
>> ULPs and it's never compounded.) You could even add syntactic sugar
>> and override things like __add__ and so on so that you can keep using
>> +, -, etc.
>>
>> Thoughts?
>>
>>    -ilia
>>
>>
>> On Fri, Feb 27, 2015 at 6:02 PM, Ilia Mirkin <imirkin at alum.mit.edu> wrote:
>>>
>>> On Fri, Feb 27, 2015 at 5:36 PM, Micah Fedke
>>> <micah.fedke at collabora.co.uk> wrote:
>>>>
>>>> +def _gen_tolerance(name, rettype, args):
>>>> +    """Return the tolerance that should be allowed for a function for
>>>> the
>>>> +    test vector passed in.  Return -1 for any vectors that would push
>>>> the
>>>> +    tolerance outside of acceptable bounds
>>>
>>>
>>> It seems to alternatively return scalars or lists. Why?
>>>
>>>> +    """
>>>> +    if name in simple_fns:
>>>> +        return simple_fns[name]
>>>> +    elif name in complex_fns:
>>>> +        if name in componentwise:
>>>> +            # for componentwise functions, call the reference function
>>>> +            # for each component (scalar components multiplex here)
>>>> +            # eg. for fn(float a, vec2 b, vec2 c) call:
>>>> +            # _fn_ref(a, b[0], c[0])
>>>> +            # _fn_ref(a, b[1], c[1])
>>>> +            # and then capture the results in a list
>>>> +            errors = []
>>>> +            badlands = []
>>>> +            component_tolerances = []
>>>> +            for component in range(rettype.num_cols *
>>>> rettype.num_rows):
>>>> +                current_args = []
>>>> +                for arg in args:
>>>> +                    # sanitize args so that all analyze operations can
>>>> be performed on a list
>>>> +                    sanitized_arg = _listify(arg)
>>>> +                    current_args.append(sanitized_arg[component %
>>>> len(sanitized_arg)])
>>>
>>>
>>> This is confusing. Does this ever happen on matrix "outputs"? When
>>> would component be > len(sanitized_arg)? And when it is, why does
>>> modding it by the length again make sense?
>>>
>>>> +                cur_errors, cur_badlands, cur_component_tolerances =
>>>> _analyze_ref_fn(complex_fns[name], current_args)
>>>> +                errors.extend(cur_errors)
>>>> +                badlands.extend(cur_badlands)
>>>> +                component_tolerances.extend(cur_component_tolerances)
>>>> +        else:
>>>> +            # for non-componentwise functions, all args must be passed
>>>> in in a single run
>>>> +            # sanitize args so that all analyze operations can be
>>>> performed on a list
>>>
>>>
>>> Why did you choose to split these up? Why not just make the various
>>> functions just always take a list and operate on the whole list? It
>>> seems like that would save a lot of heartache and confusion...
>>>
>>>> +            current_args = map(_listify, args)
>>>> +            errors, badlands, component_tolerances =
>>>> _analyze_ref_fn(complex_fns[name], current_args)
>>>> +        # results are in a list, and may have one or more members
>>>> +        return -1.0 if True in badlands else map(float,
>>>> component_tolerances)
>>>
>>>
>>> If badlands is just a true/false (as it seems to be), makes sense to
>>> not keep it as a list and just have it as a plain bool right?
>>>
>>> Also the
>>>
>>> a if b else c
>>>
>>> syntax is generally meant for "a is the super-common case, but in
>>> these awkward situations, it may be c". Or "I _really_ _really_ want a
>>> single expression here". It doesn't seem like it'd reduce readability
>>> to just do the more common
>>>
>>> if True in badlands:
>>>    return -1
>>> else:
>>>    return map(float, component_tolerances)
>>>
>>> Which also makes it more obvious that something funky is going on
>>> since one path returns a scalar while the other returns a list.
>>>
>>>> +    elif name in mult_fns:
>>>> +        x_type = glsl_type_of(args[0])
>>>> +        y_type = glsl_type_of(args[1])
>>>> +        # if matrix types are involved, the multiplication will be
>>>> matrix multiplication
>>>> +        # and we will have to pass the args through a reference
>>>> function
>>>> +        if x_type.is_vector and y_type.is_matrix:
>>>> +            mult_func = _vec_times_mat_ref
>>>> +        elif x_type.is_matrix and y_type.is_vector:
>>>> +            mult_func = _mat_times_vec_ref
>>>> +        elif x_type.is_matrix and y_type.is_matrix:
>>>> +            mult_func = _mat_times_mat_ref
>>>> +        else:
>>>> +            # float*float, float*vec, vec*float or vec*vec are all done
>>>> as
>>>> +            # normal multiplication and share a single tolerance
>>>> +            return mult_fns[name]
>>>> +        # sanitize args so that all analyze operations can be performed
>>>> on a list
>>>> +        errors, badlands, component_tolerances =
>>>> _analyze_ref_fn(mult_func, _listify(args))
>>>> +        # results are in a list, and may have one or more members
>>>> +        return -1.0 if True in badlands else map(float,
>>>> component_tolerances)
>>>> +    else:
>>>> +        # default for any operations without specified tolerances
>>>> +        return 0.0
>>>
>>>
>>> We've had a few rounds of these reviews, and I'm still generally
>>> confused by the specifics of how this is done. I normally don't have
>>> this much trouble reading code... not sure what's going on. Feel free
>>> to find someone else to review if you're getting frustrated by my
>>> apparently irreparable state of confusion.
>>>
>>> Cheers,
>>>
>>>    -ilia
>
>
> --
>
> Micah Fedke
> Collabora Ltd.
> +44 1223 362967
> https://www.collabora.com/
> https://twitter.com/collaboraltd


More information about the Piglit mailing list