Skip to content

Commit 20a92b7

Browse files
committed
GPU: give Metal an IEEE-754 binary64 in software
Metal Shading Language has no double at all, and the tracking needs one in a handful of places: the track parametrisation, the propagator and the material budget all carry double members and do double arithmetic. GPUCommonDoubleBinary64.h implements binary64 on top of a 64-bit integer. Round to nearest even only, with subnormals, infinities and NaNs, and NaN propagation in the ARM64 order, so an Apple host is a bit-exact reference down to the payload. Addition, subtraction, multiplication, division and the conversions to and from float and the 32-bit integers are exact; sin and cos are fdlibm's and land within 2 ulp of libm. There is no fused multiply-add and no square root. GPUCommonMath's own double helpers cannot serve the class: SinCosd's OpenCL arm passes the cosine by pointer where MSL takes a thread reference, so it does not compile, and Abs<double> is worse because it does -- fabs resolves to metal::fabs(float) through the implicit conversion, with no error and no warning, rounding the mantissa away at 17 call sites including the SMatrixGPU pivot selection and the Kalman guards against Almost0 and Almost1. Both are defined for Metal in GPUCommonDouble.h instead, and a static_assert there turns the silent case into a compile error, since metal::fabs is not constant-evaluable. The Metal entry point aliases the double keyword to the class, so the shared headers go on saying double and no call site changes. The header refuses to build anywhere else, since every other backend has a real double. It costs of the order of a hundred times plain float on an M-series GPU, which the tracking can afford because double is a small fraction of its floating point work. In exchange a Metal build reproduces the CPU result bit for bit wherever it uses only the exact operations.
1 parent 93e2a54 commit 20a92b7

4 files changed

Lines changed: 648 additions & 6 deletions

File tree

‎DataFormats/Reconstruction/src/TrackParametrizationWithError.cxx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ GPUd() bool TrackParametrizationWithError<value_T>::propagateTo(value_t xk, Trac
180180
}
181181
value_t kb = bz * constants::math::B2C;
182182
// evaluate in double prec.
183-
double snpRef0 = linRef0.getSnp(), cspRef0 = gpu::CAMath::Sqrt((1 - snpRef0) * (1 + snpRef0));
184-
double snpRef1 = linRef1.getSnp(), cspRef1 = gpu::CAMath::Sqrt((1 - snpRef1) * (1 + snpRef1));
185-
double cspRef0Inv = 1 / cspRef0, cspRef1Inv = 1 / cspRef1, cc = cspRef0 + cspRef1, ccInv = 1 / cc, dy2dx = (snpRef0 + snpRef1) * ccInv;
186-
double dxccInv = dx * ccInv, hh = dxccInv * cspRef1Inv * (1 + cspRef0 * cspRef1 + snpRef0 * snpRef1), jj = dx * (dy2dx - snpRef1 * cspRef1Inv);
183+
double snpRef0 = linRef0.getSnp(), cspRef0 = gpu::CAMath::Sqrt((1.f - snpRef0) * (1.f + snpRef0));
184+
double snpRef1 = linRef1.getSnp(), cspRef1 = gpu::CAMath::Sqrt((1.f - snpRef1) * (1.f + snpRef1));
185+
double cspRef0Inv = 1.f / cspRef0, cspRef1Inv = 1.f / cspRef1, cc = cspRef0 + cspRef1, ccInv = 1.f / cc, dy2dx = (snpRef0 + snpRef1) * ccInv;
186+
double dxccInv = dx * ccInv, hh = dxccInv * cspRef1Inv * (1.f + cspRef0 * cspRef1 + snpRef0 * snpRef1), jj = dx * (dy2dx - snpRef1 * cspRef1Inv);
187187

188188
double f02 = hh * cspRef0Inv;
189189
double f04 = hh * dxccInv * kb;
@@ -638,7 +638,7 @@ GPUd() bool TrackParametrizationWithError<value_T>::propagateTo(value_t xk, cons
638638
}
639639
double r1pr2Inv = 1. / (r1 + r2), r2inv = 1. / r2, r1inv = 1. / r1;
640640
double dy2dx = (f1 + f2) * r1pr2Inv, dx2r1pr2 = dx * r1pr2Inv;
641-
value_t step = (gpu::CAMath::Abs(x2r) < 0.05f) ? dx * gpu::CAMath::Abs(r2 + f2 * dy2dx) // chord
641+
value_t step = (gpu::CAMath::Abs(x2r) < 0.05f) ? value_t(dx * gpu::CAMath::Abs(r2 + f2 * dy2dx)) // chord
642642
: 2.f * gpu::CAMath::ASin(0.5f * dx * gpu::CAMath::Sqrt(1.f + dy2dx * dy2dx) * crv) / crv; // arc
643643
step *= gpu::CAMath::Sqrt(1.f + this->getTgl() * this->getTgl());
644644
//
@@ -1086,7 +1086,7 @@ GPUd() auto TrackParametrizationWithError<value_T>::getPredictedChi2(const value
10861086
auto chi2 = (d * (szz * d - sdz * z) + z * (sdd * z - d * sdz)) / det;
10871087
if (chi2 < 0.) {
10881088
#ifndef GPUCA_ALIGPUCODE
1089-
LOGP(warning, "Negative chi2={}, Cluster: {} {} {} Dy:{} Dz:{} | sdd:{} sdz:{} szz:{} det:{}", chi2, cov[0], cov[1], cov[2], d, z, sdd, sdz, szz, det);
1089+
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));
10901090
LOGP(warning, "Track: {}", asString());
10911091
#endif
10921092
}

‎GPU/Common/GPUCommonDouble.h‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2019-2025 CERN and copyright holders of ALICE O2.
2+
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3+
// All rights not expressly granted are reserved.
4+
//
5+
// This software is distributed under the terms of the GNU General Public
6+
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7+
//
8+
// In applying this license CERN does not waive the privileges and immunities
9+
// granted to it by virtue of its status as an Intergovernmental Organization
10+
// or submit itself to any jurisdiction.
11+
12+
/// \file GPUCommonDouble.h
13+
/// \brief Storage for double precision members shared with a device that has none
14+
15+
/// \brief What MSL needs once the double keyword names the emulated type
16+
17+
#ifndef GPUCOMMONDOUBLE_H
18+
#define GPUCOMMONDOUBLE_H
19+
20+
#include "GPUCommonDef.h"
21+
#include "GPUCommonMath.h"
22+
23+
#ifdef __METAL__
24+
25+
namespace o2::gpu
26+
{
27+
28+
static_assert(sizeof(double) == 8, "the emulated double must match the size of a real one");
29+
static_assert(alignof(double) == 8, "the emulated double must match the alignment of a real one");
30+
31+
// CAMath::Abs deduces its parameter rather than taking a float, so a call on a
32+
// double picks the primary template, which has no definition. The rest of CAMath
33+
// takes float and is reached through the implicit conversion.
34+
template <>
35+
GPUhdi() constexpr double GPUCommonMath::Abs<double>(double x)
36+
{
37+
return double::fromBits(x.bits() & ~GPUCA_B64_SIGN);
38+
}
39+
40+
// metal::fabs is not constant-evaluable, so this also fails to compile if the
41+
// specialisation above is ever dropped and the call falls back to it in float
42+
static_assert(GPUCommonMath::Abs<double>(GPUdoubleBinary64::fromBits(0xBFF0000000000001ULL)).bits() == 0x3FF0000000000001ULL,
43+
"Abs on the emulated double must clear the sign bit and keep every other one");
44+
45+
} // namespace o2::gpu
46+
47+
#endif // __METAL__
48+
49+
#endif // GPUCOMMONDOUBLE_H

0 commit comments

Comments
 (0)