Skip to content

Commit a5fad7b

Browse files
committed
GPU: make GPUdoubleCalcImpl cheaper and more accurate
Three changes, measured on the full propagateTo kernel over 524288 tracks with strongly correlated covariances, against the CPU double result (Apple M1 Max, -fno-fast-math, median of ten runs): * mLo is no longer renormalised after each operation. The value is still mHi + mLo and nothing downstream requires |mLo| <= ulp(mHi)/2, so the results are identical to the last bit: 3.11x -> 2.22x the cost of plain float. * the mixed float forms no longer widen the float operand and run the full two-float operation. Its mLo is zero, but with reassociation disabled the compiler is not allowed to fold those terms away. * float += GPUdoubleCalcImpl now rounds once, as float += double does on the host, rather than converting to float and adding. This is what `c00 += b00 + b00 + a00` resolves to, and it is where the remaining error was: +7% cost, and the covariance error against the CPU double result drops from 1.8e-6 to 5.6e-7 of sqrt(C_ii C_jj), with 86% of elements bit-identical instead of 76%. Net: 3.11x -> 2.37x and closer to the CPU than before. For reference, plain float is at 1.2e-5 on the same metric, and the CPU double result is itself 1.1e-6 away from a cancellation-free reference, so the type is now at the noise floor of the formula it implements. Representations narrower than two floats were measured and are not viable: with four halves, limbs two to four of any value below about one fall into half subnormals, leaving fewer effective bits than a single float (worst error 0.46, 42x the cost), and fixed point Q21.42 reaches 7.4e-6 at 16x.
1 parent 26b9b60 commit a5fad7b

1 file changed

Lines changed: 60 additions & 45 deletions

File tree

‎GPU/Common/GPUCommonDouble.h‎

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,17 @@ static_assert(sizeof(GPUdoubleStore) == 8, "GPUdoubleStore must match the size o
8888
static_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; }
174178
GPUdi() 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; }
179183
GPUdi() 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

Comments
 (0)