Skip to content

Commit 170d0ce

Browse files
committed
GPU: add an emulated binary64 and a selector for GPUdoubleCalc
GPUdoubleCalc now names one of four representations, chosen by GPUCA_DOUBLECALC: hardware double, plain float, the compensated two-float type, or a full IEEE-754 binary64 in software. The default is unchanged, double everywhere except Metal and the two-float type there, so this is a no-op for every existing build. It replaces GPUCA_FORCE_DOUBLECALC and GPUCA_FORCE_FLOATCALC, which could only express two of the four. The binary64 emulation is round to nearest even with subnormals, infinities and NaNs, and correctly rounded division; NaN propagation follows the ARM64 order so an Apple host is a reference down to the payload. It is not for production: over the whole propagateTo kernel on an M1 Max it costs of the order of a hundred times plain float, against roughly two for the two-float type. It is here because it reproduces the CPU result exactly -- every covariance element bit-identical -- which turns a disagreement between Metal and the CPU into a search over code rather than over numerics. Measured on that kernel, 524288 tracks with strongly correlated covariances, median of ten runs, error as |dC_ij| / sqrt(C_ii C_jj) against the CPU double result: plain float 0.108 ns and 1.2e-5, two-float 0.256 ns and 5.6e-7, binary64 12.2 ns and zero. Only the binary64 kernel is unstable run to run, between 9.1 and 12.3 ns; the others vary by a few percent. For scale, the CPU double result is itself 1.1e-6 from a cancellation-free reference, so the two-float type is already at the noise floor of the formula. For scale in the other direction, the same propagateTo on this machine's CPU, where double is native, costs 23.5 ns in float and 27.1 ns in double, so real hardware double is a 1.15x proposition. None of this arithmetic would be needed if Metal had it. Validated against hardware double on 9M operand pairs per operation, over uniform bit patterns, near-equal exponents, the subnormal band, sparse mantissas for the exact and halfway cases, the special values and track-like magnitudes, plus 4M conversions each way: no mismatches. The arithmetic must stay out of line on Metal. Inlined into a kernel it drops the occupancy and runs two to five times slower than the call.
1 parent 7d739bc commit 170d0ce

3 files changed

Lines changed: 433 additions & 13 deletions

File tree

‎DataFormats/Reconstruction/src/TrackParametrizationWithError.cxx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,7 @@ GPUd() auto TrackParametrizationWithError<value_T>::getPredictedChi2(const value
10871087
auto chi2 = (d * (szz * d - sdz * z) + z * (sdd * z - d * sdz)) / det;
10881088
if (chi2 < 0.) {
10891089
#ifndef GPUCA_ALIGPUCODE
1090-
LOGP(warning, "Negative chi2={}, Cluster: {} {} {} Dy:{} Dz:{} | sdd:{} sdz:{} szz:{} det:{}", chi2, cov[0], cov[1], cov[2], d, z, sdd, sdz, szz, det);
1090+
LOGP(warning, "Negative chi2={}, Cluster: {} {} {} Dy:{} Dz:{} | sdd:{} sdz:{} szz:{} det:{}", double(chi2), cov[0], cov[1], cov[2], d, z, double(sdd), double(sdz), double(szz), double(det));
10911091
LOGP(warning, "Track: {}", asString());
10921092
#endif
10931093
}

‎GPU/Common/GPUCommonDouble.h‎

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include "GPUCommonDef.h"
1919
#include "GPUCommonMath.h"
20+
#include "GPUCommonDoubleBinary64.h"
2021

2122
#ifndef GPUCA_GPUCODE_DEVICE
2223
#include <cstdint>
@@ -194,27 +195,56 @@ GPUdi() threadgroup float& operator+=(threadgroup float& a, GPUdoubleCalcImpl b)
194195
GPUdi() float& operator+=(float& a, GPUdoubleCalcImpl b) { return a = (float)(GPUdoubleCalcImpl(a) + b); }
195196
#endif
196197

197-
// CAMath::Abs is a template that deduces its parameter, so a call on this type
198-
// picks the primary template, which has no definition. The rest of CAMath takes
199-
// float and is reached through the implicit conversion.
198+
// CAMath::Abs is a template that deduces its parameter, so a call on one of these
199+
// types picks the primary template, which has no definition. The rest of CAMath
200+
// takes float and is reached through the implicit conversion.
200201
template <>
201202
GPUhdi() GPUdoubleCalcImpl GPUCommonMath::Abs<GPUdoubleCalcImpl>(GPUdoubleCalcImpl x)
202203
{
203204
return (float)x < 0.f ? -x : x;
204205
}
205206

206-
// GPUCA_FORCE_DOUBLECALC lets a host test exercise the Metal representation and
207-
// compare it against the double one.
208-
#if defined(__METAL__) && defined(__FAST_MATH__)
209-
// Fast math reassociates the compensation terms away: the two-float type would
210-
// then cost 1.5x for the accuracy of a plain float.
207+
template <>
208+
GPUhdi() GPUdoubleBinary64 GPUCommonMath::Abs<GPUdoubleBinary64>(GPUdoubleBinary64 x)
209+
{
210+
return GPUdoubleBinary64::fromBits(x.bits() & ~GPUCA_B64_SIGN);
211+
}
212+
213+
// What GPUdoubleCalc is. Define GPUCA_DOUBLECALC to one of these to override the
214+
// default, which is hardware double everywhere except Metal, and the compensated
215+
// two-float type there. A host build can be set to any of them to compare the
216+
// representations against each other; a Metal build can be set to BINARY64 to
217+
// reproduce the CPU result bit for bit, at two orders of magnitude the cost.
218+
#define GPUCA_DOUBLECALC_DOUBLE 1
219+
#define GPUCA_DOUBLECALC_FLOAT 2
220+
#define GPUCA_DOUBLECALC_TWOFLOAT 3
221+
#define GPUCA_DOUBLECALC_BINARY64 4
222+
223+
#ifndef GPUCA_DOUBLECALC
224+
#if defined(__METAL__) && defined(__FAST_MATH__)
225+
// Fast math reassociates the compensation terms away, so the two-float type
226+
// would cost 1.5x for the accuracy of a plain float.
227+
#define GPUCA_DOUBLECALC GPUCA_DOUBLECALC_FLOAT
228+
#elif defined(__METAL__)
229+
#define GPUCA_DOUBLECALC GPUCA_DOUBLECALC_TWOFLOAT
230+
#else
231+
#define GPUCA_DOUBLECALC GPUCA_DOUBLECALC_DOUBLE
232+
#endif
233+
#endif
234+
235+
#if GPUCA_DOUBLECALC == GPUCA_DOUBLECALC_DOUBLE
236+
#ifdef __METAL__
237+
#error "MSL has no double; GPUCA_DOUBLECALC_DOUBLE cannot be selected for Metal"
238+
#endif
239+
typedef double GPUdoubleCalc;
240+
#elif GPUCA_DOUBLECALC == GPUCA_DOUBLECALC_FLOAT
211241
typedef float GPUdoubleCalc;
212-
#elif defined(__METAL__) || defined(GPUCA_FORCE_DOUBLECALC)
242+
#elif GPUCA_DOUBLECALC == GPUCA_DOUBLECALC_TWOFLOAT
213243
typedef GPUdoubleCalcImpl GPUdoubleCalc;
214-
#elif defined(GPUCA_FORCE_FLOATCALC) // for the host test only, to show what plain float would cost
215-
typedef float GPUdoubleCalc;
244+
#elif GPUCA_DOUBLECALC == GPUCA_DOUBLECALC_BINARY64
245+
typedef GPUdoubleBinary64 GPUdoubleCalc;
216246
#else
217-
typedef double GPUdoubleCalc;
247+
#error "Invalid setting for GPUCA_DOUBLECALC"
218248
#endif
219249

220250
} // namespace o2::gpu

0 commit comments

Comments
 (0)