@@ -88,12 +88,17 @@ static_assert(sizeof(GPUdoubleStore) == 8, "GPUdoubleStore must match the size o
8888static_assert (alignof (GPUdoubleStore) == 8 , " GPUdoubleStore must match the alignment of a double" );
8989
9090
91- // Compensated two-float arithmetic, for the intermediates that are deliberately
92- // computed in double even when the track itself is float -- the Jacobian terms in
93- // TrackParametrizationWithError::propagateTo and friends, where differences of
94- // nearly equal quantities cancel. Plain float loses up to ~1e-2 relative there;
95- // this representation, value = mHi + mLo, holds the error term explicitly and
96- // stays below 1e-9 across the same inputs.
91+ // Compensated two-float arithmetic, value = mHi + mLo, for the intermediates that
92+ // are deliberately computed in double even when the track itself is float -- the
93+ // Jacobian and covariance terms in TrackParametrizationWithError::propagateTo and
94+ // friends, where differences of nearly equal quantities cancel.
95+ //
96+ // Every operation here depends on the compiler not reassociating the compensation
97+ // terms away, which is why the Metal device code is built without fast math; when
98+ // it is built with fast math, GPUdoubleCalc below is a plain float instead.
99+ //
100+ // mLo is not renormalised after each operation: the value is still mHi + mLo and
101+ // nothing downstream requires |mLo| <= ulp(mHi)/2.
97102//
98103// Defined for every backend so it can be tested on the host, but only Metal uses
99104// it: everywhere else GPUdoubleCalc is a plain double.
@@ -110,74 +115,84 @@ class GPUdoubleCalcImpl
110115 GPUdi () GPUdoubleCalcImpl operator -() const { return GPUdoubleCalcImpl (-mHi , -mLo ); }
111116 GPUdi () GPUdoubleCalcImpl operator +(GPUdoubleCalcImpl b) const
112117 {
113- GPUdoubleCalcImpl s = twoSum (mHi , b.mHi );
114- s.mLo += mLo + b.mLo ;
115- return quickTwoSum (s.mHi , s.mLo );
118+ const float s = mHi + b.mHi , bb = s - mHi ;
119+ return GPUdoubleCalcImpl (s, ((mHi - (s - bb)) + (b.mHi - bb)) + (mLo + b.mLo ));
116120 }
117121 GPUdi () GPUdoubleCalcImpl operator -(GPUdoubleCalcImpl b) const { return *this + (-b); }
118122 GPUdi () GPUdoubleCalcImpl operator *(GPUdoubleCalcImpl b) const
119123 {
120- GPUdoubleCalcImpl p = twoProd (mHi , b.mHi );
121- p.mLo += mHi * b.mLo + mLo * b.mHi ;
122- return quickTwoSum (p.mHi , p.mLo );
124+ const float p = mHi * b.mHi ;
125+ return GPUdoubleCalcImpl (p, __builtin_fmaf (mHi , b.mHi , -p) + (mHi * b.mLo + mLo * b.mHi ));
123126 }
124127 GPUdi () GPUdoubleCalcImpl operator /(GPUdoubleCalcImpl b) const
125128 {
126- const float q1 = mHi / b.mHi ;
127- const GPUdoubleCalcImpl d = * this - GPUdoubleCalcImpl (q1) * b;
128- return quickTwoSum (q1, (d. mHi + d. mLo ) / b.mHi );
129+ const float q = mHi / b.mHi ;
130+ const float r = ( __builtin_fmaf (-q, b. mHi , mHi ) + mLo ) - q * b. mLo ;
131+ return GPUdoubleCalcImpl (q, r / b.mHi );
129132 }
133+
130134 // exact matches for the mixed forms, so `a * someFloat` does not sit ambiguously
131- // between converting the float up and converting *this down
135+ // between converting the float up and converting *this down. They also skip the
136+ // mLo terms that are zero for a float operand, which no reassociation is allowed
137+ // to fold away here.
138+ GPUdi () GPUdoubleCalcImpl operator +(float b) const
139+ {
140+ const float s = mHi + b, bb = s - mHi ;
141+ return GPUdoubleCalcImpl (s, ((mHi - (s - bb)) + (b - bb)) + mLo );
142+ }
143+ GPUdi () GPUdoubleCalcImpl operator -(float b) const { return *this + (-b); }
144+ GPUdi () GPUdoubleCalcImpl operator *(float b) const
145+ {
146+ const float p = mHi * b;
147+ return GPUdoubleCalcImpl (p, __builtin_fmaf (mHi , b, -p) + mLo * b);
148+ }
149+ GPUdi () GPUdoubleCalcImpl operator /(float b) const
150+ {
151+ const float q = mHi / b;
152+ const float r = __builtin_fmaf (-q, b, mHi ) + mLo ;
153+ return GPUdoubleCalcImpl (q, r / b);
154+ }
132155 // MSL has no double, so on Metal a literal like `1.` is already float and only
133156 // the float forms are ever selected. The double forms exist so the type can be
134157 // compiled and tested on the host, where such literals really are double.
135158#ifndef __METAL__
136- GPUdi () GPUdoubleCalcImpl operator+(double b) const { return *this + GPUdoubleCalcImpl (( float )b) ; }
137- GPUdi () GPUdoubleCalcImpl operator-(double b) const { return *this - GPUdoubleCalcImpl (( float )b) ; }
138- GPUdi () GPUdoubleCalcImpl operator*(double b) const { return *this * GPUdoubleCalcImpl (( float )b) ; }
139- GPUdi () GPUdoubleCalcImpl operator/(double b) const { return *this / GPUdoubleCalcImpl (( float )b) ; }
159+ GPUdi () GPUdoubleCalcImpl operator+(double b) const { return *this + ( float )b; }
160+ GPUdi () GPUdoubleCalcImpl operator-(double b) const { return *this - ( float )b; }
161+ GPUdi () GPUdoubleCalcImpl operator*(double b) const { return *this * ( float )b; }
162+ GPUdi () GPUdoubleCalcImpl operator/(double b) const { return *this / ( float )b; }
140163#endif
141- GPUdi () GPUdoubleCalcImpl operator +(float b) const { return *this + GPUdoubleCalcImpl (b); }
142- GPUdi () GPUdoubleCalcImpl operator -(float b) const { return *this - GPUdoubleCalcImpl (b); }
143- GPUdi () GPUdoubleCalcImpl operator *(float b) const { return *this * GPUdoubleCalcImpl (b); }
144- GPUdi () GPUdoubleCalcImpl operator /(float b) const { return *this / GPUdoubleCalcImpl (b); }
145164
146165 GPUdi () GPUdoubleCalcImpl& operator +=(GPUdoubleCalcImpl b) { return *this = *this + b; }
147166 GPUdi () GPUdoubleCalcImpl& operator -=(GPUdoubleCalcImpl b) { return *this = *this - b; }
148167 GPUdi () GPUdoubleCalcImpl& operator *=(GPUdoubleCalcImpl b) { return *this = *this * b; }
149168 GPUdi () GPUdoubleCalcImpl& operator /=(GPUdoubleCalcImpl b) { return *this = *this / b; }
150169
151170 private:
152- GPUdi () static GPUdoubleCalcImpl twoSum (float a, float b)
153- {
154- const float s = a + b, bb = s - a;
155- return GPUdoubleCalcImpl (s, (a - (s - bb)) + (b - bb));
156- }
157- GPUdi () static GPUdoubleCalcImpl quickTwoSum (float a, float b)
158- {
159- const float s = a + b;
160- return GPUdoubleCalcImpl (s, b - (s - a));
161- }
162- GPUdi () static GPUdoubleCalcImpl twoProd (float a, float b)
163- {
164- const float p = a * b;
165- return GPUdoubleCalcImpl (p, __builtin_fmaf (a, b, -p));
166- }
167171 float mHi , mLo ;
168172};
169173
170174#ifndef __METAL__
171- GPUdi () GPUdoubleCalcImpl operator+(double a, GPUdoubleCalcImpl b) { return GPUdoubleCalcImpl (( float )a) + b ; }
172- GPUdi () GPUdoubleCalcImpl operator-(double a, GPUdoubleCalcImpl b) { return GPUdoubleCalcImpl ( (float )a) - b ; }
173- GPUdi () GPUdoubleCalcImpl operator*(double a, GPUdoubleCalcImpl b) { return GPUdoubleCalcImpl (( float )a) * b ; }
175+ GPUdi () GPUdoubleCalcImpl operator+(double a, GPUdoubleCalcImpl b) { return b + ( float )a; }
176+ GPUdi () GPUdoubleCalcImpl operator-(double a, GPUdoubleCalcImpl b) { return (-b) + (float )a; }
177+ GPUdi () GPUdoubleCalcImpl operator*(double a, GPUdoubleCalcImpl b) { return b * ( float )a; }
174178GPUdi () GPUdoubleCalcImpl operator/(double a, GPUdoubleCalcImpl b) { return GPUdoubleCalcImpl ((float )a) / b; }
175179#endif
176- GPUdi () GPUdoubleCalcImpl operator +(float a, GPUdoubleCalcImpl b) { return GPUdoubleCalcImpl (a) + b ; }
177- GPUdi () GPUdoubleCalcImpl operator -(float a, GPUdoubleCalcImpl b) { return GPUdoubleCalcImpl (a) - b ; }
178- GPUdi () GPUdoubleCalcImpl operator *(float a, GPUdoubleCalcImpl b) { return GPUdoubleCalcImpl (a) * b ; }
180+ GPUdi () GPUdoubleCalcImpl operator +(float a, GPUdoubleCalcImpl b) { return b + a ; }
181+ GPUdi () GPUdoubleCalcImpl operator -(float a, GPUdoubleCalcImpl b) { return (-b) + a ; }
182+ GPUdi () GPUdoubleCalcImpl operator *(float a, GPUdoubleCalcImpl b) { return b * a ; }
179183GPUdi () GPUdoubleCalcImpl operator /(float a, GPUdoubleCalcImpl b) { return GPUdoubleCalcImpl (a) / b; }
180184
185+ // rounds once, as `someFloat += someDouble` does on the host. MSL resolves the
186+ // address spaces separately, and a generic reference would tie with the built-in
187+ // float += float rather than beat it.
188+ #ifdef __METAL__
189+ GPUdi () thread float& operator+=(thread float & a, GPUdoubleCalcImpl b) { return a = (float )(GPUdoubleCalcImpl (a) + b); }
190+ GPUdi () device float& operator+=(device float & a, GPUdoubleCalcImpl b) { return a = (float )(GPUdoubleCalcImpl (a) + b); }
191+ GPUdi () threadgroup float& operator+=(threadgroup float & a, GPUdoubleCalcImpl b) { return a = (float )(GPUdoubleCalcImpl (a) + b); }
192+ #else
193+ GPUdi () float& operator+=(float & a, GPUdoubleCalcImpl b) { return a = (float )(GPUdoubleCalcImpl (a) + b); }
194+ #endif
195+
181196// GPUCA_FORCE_DOUBLECALC lets a host test exercise the Metal representation and
182197// compare it against the double one.
183198#if defined(__METAL__) && defined(__FAST_MATH__)
0 commit comments