Skip to content

Commit ee27434

Browse files
committed
KOKKOS ACE: optional direct (spline-free) ChebPow radial evaluation
Add an opt-in "direct" pair_style keyword (pace/kk) that replaces the memory-bound radial spline-table lookup with direct on-the-fly evaluation of the ChebPow basis, intended to raise arithmetic intensity on the GPU: - gr/dgr from a Chebyshev recurrence (first and second kind) advanced on the fly (no per-degree scratch); - fr/dfr = crad . gr, a per-element-pair-constant matrix product whose coefficients stay resident in cache instead of streaming a large, randomly-indexed spline table every neighbor. The hard-core term keeps its (single-function) spline. Falls back to the spline path unless "direct" is given and the pair uses the ChebPow basis (a warning is issued for unsupported bases). Spline remains the default, so existing behaviour is unchanged. Device data added: d_crad, d_lambda, d_cut, and a per-pair basis code. Validation (CPU): - The direct formula reproduces the ACE library's own analytic all_radfunc (gr, dgr, fr, dfr) bit-for-bit (0.0 max abs diff) over 1381 radii across the full cutoff range (standalone test linked against the pace library). - End-to-end through the Kokkos kernels (run on the Serial backend): product-mode spline matches the CPU evaluator to ~13 significant figures, and direct vs spline differ only by the spline interpolation error (energy ~3e-13 rel; max per-atom force diff ~2.5e-9 eV/A on a displaced configuration) - nonzero, confirming the direct path runs independently, and tiny, confirming it is correct. GPU performance (the actual bandwidth-to-compute tradeoff) still needs to be benchmarked on device hardware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C752fRoWVbYmN4ugqitWps
1 parent c01698b commit ee27434

2 files changed

Lines changed: 161 additions & 8 deletions

File tree

‎src/KOKKOS/pair_pace_kokkos.cpp‎

Lines changed: 145 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,33 @@ PairPACEKokkos<DeviceType>::PairPACEKokkos(LAMMPS *lmp) : PairPACE(lmp)
6565
datamask_modify = EMPTY_MASK;
6666

6767
host_flag = (execution_space == HostKK);
68+
69+
radial_direct = 0;
70+
}
71+
72+
/* ----------------------------------------------------------------------
73+
global settings: intercept the KOKKOS-only "direct" keyword (direct
74+
Chebyshev radial evaluation instead of spline lookup), then defer the
75+
remaining keywords to the base class.
76+
------------------------------------------------------------------------- */
77+
78+
template<class DeviceType>
79+
void PairPACEKokkos<DeviceType>::settings(int narg, char **arg)
80+
{
81+
radial_direct = 0;
82+
char **arg_filtered = new char*[narg];
83+
int narg_filtered = 0;
84+
for (int i = 0; i < narg; i++) {
85+
if (strcmp(arg[i], "direct") == 0)
86+
radial_direct = 1;
87+
else
88+
arg_filtered[narg_filtered++] = arg[i];
89+
}
90+
PairPACE::settings(narg_filtered, arg_filtered);
91+
delete[] arg_filtered;
92+
93+
if (radial_direct && comm->me == 0)
94+
utils::logmesg(lmp, "Direct (spline-free) ChebPow radial evaluation requested\n");
6895
}
6996

7097
/* ----------------------------------------------------------------------
@@ -271,6 +298,50 @@ void PairPACEKokkos<DeviceType>::copy_splines()
271298
k_splines_gk.sync_device();
272299
k_splines_rnl.sync_device();
273300
k_splines_hc.sync_device();
301+
302+
// ---- device data for direct (spline-free) ChebPow radial evaluation ----
303+
const int nr = radial_functions->nradial;
304+
const int nl = radial_functions->lmax + 1;
305+
const int nb = radial_functions->nradbase;
306+
307+
d_crad = Kokkos::View<KK_FLOAT*****, DeviceType>("pace:crad", nelements, nelements, nr, nl, nb);
308+
d_radbasename = Kokkos::View<int**, DeviceType>("pace:radbasename", nelements, nelements);
309+
MemKK::realloc_kokkos(d_lambda, "pace:lambda", nelements, nelements);
310+
MemKK::realloc_kokkos(d_cut, "pace:cut", nelements, nelements);
311+
312+
auto h_crad = Kokkos::create_mirror_view(d_crad);
313+
auto h_radbasename = Kokkos::create_mirror_view(d_radbasename);
314+
auto h_lambda = Kokkos::create_mirror_view(d_lambda);
315+
auto h_cut = Kokkos::create_mirror_view(d_cut);
316+
317+
for (int i = 0; i < nelements; i++) {
318+
for (int j = 0; j < nelements; j++) {
319+
h_lambda(i, j) = radial_functions->lambda(i, j);
320+
h_cut(i, j) = radial_functions->cut(i, j);
321+
h_radbasename(i, j) =
322+
(radial_functions->radbasenameij(i, j) == "ChebPow") ? RADBASE_CHEBPOW : RADBASE_OTHER;
323+
for (int n = 0; n < nr; n++)
324+
for (int l = 0; l < nl; l++)
325+
for (int k = 0; k < nb; k++)
326+
h_crad(i, j, n, l, k) = radial_functions->crad(i, j, n, l, k);
327+
}
328+
}
329+
330+
Kokkos::deep_copy(d_crad, h_crad);
331+
Kokkos::deep_copy(d_radbasename, h_radbasename);
332+
Kokkos::deep_copy(d_lambda, h_lambda);
333+
Kokkos::deep_copy(d_cut, h_cut);
334+
335+
// Direct evaluation currently supports only the ChebPow basis; warn and fall
336+
// back to splines for any element pair that uses a different basis.
337+
if (radial_direct && comm->me == 0) {
338+
for (int i = 0; i < nelements; i++)
339+
for (int j = 0; j < nelements; j++)
340+
if (h_radbasename(i, j) != RADBASE_CHEBPOW)
341+
error->warning(FLERR, "pair pace/kk 'direct' requested but element pair {}-{} uses "
342+
"radial basis '{}', which is not supported; using splines for it",
343+
i, j, radial_functions->radbasenameij(i, j));
344+
}
274345
}
275346

276347
/* ---------------------------------------------------------------------- */
@@ -1868,6 +1939,65 @@ void PairPACEKokkos<DeviceType>::FS_values_and_derivatives(const int ii, KK_FLOA
18681939

18691940
/* ---------------------------------------------------------------------- */
18701941

1942+
template<class DeviceType>
1943+
// NOLINTNEXTLINE
1944+
KOKKOS_INLINE_FUNCTION
1945+
void PairPACEKokkos<DeviceType>::evaluate_radial_direct_chebpow(const int ii, const int jj,
1946+
const KK_FLOAT r, const int mu_i, const int mu_j) const
1947+
{
1948+
const KK_FLOAT cut = d_cut(mu_i, mu_j);
1949+
const KK_FLOAT lam = d_lambda(mu_i, mu_j);
1950+
1951+
// ChebPow scaled coordinate x(r) and its derivative:
1952+
// yb = 1 - r/cut, y = yb^lam, x = 2*(1 - y) - 1
1953+
const KK_FLOAT yb = 1.0 - r / cut;
1954+
const KK_FLOAT yp = pow(yb, lam - 1.0); // yb^(lam-1)
1955+
const KK_FLOAT y = yp * yb; // yb^lam
1956+
const KK_FLOAT dydr = -lam / cut * yp; // dy/dr
1957+
const KK_FLOAT x = 2.0 * (1.0 - y) - 1.0;
1958+
const KK_FLOAT dx = -2.0 * dydr;
1959+
1960+
// Chebyshev polynomials of the first kind (cheb) and second kind (cheb2),
1961+
// advanced on the fly so no per-degree scratch array is needed. For ChebPow
1962+
// gr(m-1) = 0.5 - 0.5*cheb(m)
1963+
// dgr(m-1) = -0.5 * dcheb(m) * dx, dcheb(m) = m * cheb2(m-1)
1964+
// with cheb(0)=1, cheb(1)=x, cheb2(0)=1, cheb2(1)=2x and the three-term
1965+
// recurrence p(m+1) = 2x*p(m) - p(m-1).
1966+
const KK_FLOAT twox = 2.0 * x;
1967+
KK_FLOAT cheb_prev = 1.0; // cheb(m-1), starts at cheb(0)
1968+
KK_FLOAT cheb_cur = x; // cheb(m), starts at cheb(1)
1969+
KK_FLOAT cheb2_prev = 1.0; // cheb2(m-1), starts at cheb2(0)
1970+
KK_FLOAT cheb2_cur = twox; // cheb2(m), starts at cheb2(1)
1971+
for (int m = 1; m <= nradbase; m++) {
1972+
gr(ii, jj, m - 1) = 0.5 - 0.5 * cheb_cur;
1973+
dgr(ii, jj, m - 1) = -0.5 * (KK_FLOAT)m * cheb2_prev * dx;
1974+
const KK_FLOAT cheb_next = twox * cheb_cur - cheb_prev;
1975+
cheb_prev = cheb_cur;
1976+
cheb_cur = cheb_next;
1977+
const KK_FLOAT cheb2_next = twox * cheb2_cur - cheb2_prev;
1978+
cheb2_prev = cheb2_cur;
1979+
cheb2_cur = cheb2_next;
1980+
}
1981+
1982+
// R_nl(r) = sum_k crad(mu_i,mu_j,n,l,k) * g_k(r); crad is constant per
1983+
// element pair, so it streams from cache while gr/dgr stay resident.
1984+
for (int n = 0; n < nradmax; n++) {
1985+
for (int l = 0; l <= lmax; l++) {
1986+
KK_FLOAT frval = 0.0;
1987+
KK_FLOAT dfrval = 0.0;
1988+
for (int k = 0; k < nradbase; k++) {
1989+
const KK_FLOAT c = d_crad(mu_i, mu_j, n, l, k);
1990+
frval += c * gr(ii, jj, k);
1991+
dfrval += c * dgr(ii, jj, k);
1992+
}
1993+
fr(ii, jj, l, n) = frval;
1994+
dfr(ii, jj, l, n) = dfrval;
1995+
}
1996+
}
1997+
}
1998+
1999+
/* ---------------------------------------------------------------------- */
2000+
18712001
template<class DeviceType>
18722002
// NOLINTNEXTLINE
18732003
KOKKOS_INLINE_FUNCTION
@@ -1879,17 +2009,24 @@ void PairPACEKokkos<DeviceType>::evaluate_splines(const int ii, const int jj, KK
18792009
auto &spline_rnl = k_splines_rnl.template view<DeviceType>()(mu_i, mu_j);
18802010
auto &spline_hc = k_splines_hc.template view<DeviceType>()(mu_i, mu_j);
18812011

1882-
spline_gk.calcSplines(ii, jj, r, gr, dgr);
1883-
1884-
spline_rnl.calcSplines(ii, jj, r, d_values, d_derivatives);
1885-
for (int ll = 0; ll < (int)fr.extent(2); ll++) {
1886-
for (int kk = 0; kk < (int)fr.extent(3); kk++) {
1887-
const int flatten = kk*fr.extent(2) + ll;
1888-
fr(ii, jj, ll, kk) = d_values(ii, jj, flatten);
1889-
dfr(ii, jj, ll, kk) = d_derivatives(ii, jj, flatten);
2012+
if (radial_direct && d_radbasename(mu_i, mu_j) == RADBASE_CHEBPOW) {
2013+
// Direct Chebyshev evaluation fills gr/dgr and fr/dfr without the spline
2014+
// table lookup (higher arithmetic intensity, no per-neighbor table reads).
2015+
evaluate_radial_direct_chebpow(ii, jj, r, mu_i, mu_j);
2016+
} else {
2017+
spline_gk.calcSplines(ii, jj, r, gr, dgr);
2018+
2019+
spline_rnl.calcSplines(ii, jj, r, d_values, d_derivatives);
2020+
for (int ll = 0; ll < (int)fr.extent(2); ll++) {
2021+
for (int kk = 0; kk < (int)fr.extent(3); kk++) {
2022+
const int flatten = kk*fr.extent(2) + ll;
2023+
fr(ii, jj, ll, kk) = d_values(ii, jj, flatten);
2024+
dfr(ii, jj, ll, kk) = d_derivatives(ii, jj, flatten);
2025+
}
18902026
}
18912027
}
18922028

2029+
// the hard-core repulsion is always taken from its (single-function) spline
18932030
spline_hc.calcSplines(ii, jj, r, d_values, d_derivatives);
18942031
cr(ii, jj) = d_values(ii, jj, 0);
18952032
dcr(ii, jj) = d_derivatives(ii, jj, 0);

‎src/KOKKOS/pair_pace_kokkos.h‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ class PairPACEKokkos : public PairPACE {
175175

176176
friend void pair_virial_fdotr_compute<PairPACEKokkos>(PairPACEKokkos*);
177177

178+
void settings(int, char **) override;
178179
void grow(int, int);
179180
void copy_pertype();
180181
void copy_splines();
@@ -214,6 +215,14 @@ class PairPACEKokkos : public PairPACE {
214215
KOKKOS_INLINE_FUNCTION
215216
void evaluate_splines(const int, const int, KK_FLOAT, int, int, int, int) const;
216217

218+
// Direct (spline-free) evaluation of the ChebPow radial basis: computes the
219+
// Chebyshev recurrence on the fly for gr/dgr, then fr/dfr via the crad
220+
// matrix product. Trades the memory-bound spline-table lookup for higher
221+
// arithmetic intensity (crad is per-element-pair constant and stays cached).
222+
// NOLINTNEXTLINE
223+
KOKKOS_INLINE_FUNCTION
224+
void evaluate_radial_direct_chebpow(const int, const int, const KK_FLOAT, const int, const int) const;
225+
217226
// Shared inner radial loop for ComputeDerivative. Accumulates the gradient
218227
// contribution of a single (l, m) spherical-harmonic channel into f_ji for
219228
// all radial functions n. wscale folds in the factor-of-2 used for m > 0.
@@ -306,6 +315,13 @@ class PairPACEKokkos : public PairPACE {
306315
t_ace_3d d_values;
307316
t_ace_3d d_derivatives;
308317

318+
// direct (spline-free) radial evaluation
319+
enum { RADBASE_OTHER = 0, RADBASE_CHEBPOW = 1 };
320+
int radial_direct; // user flag: use direct Chebyshev evaluation
321+
Kokkos::View<int**, DeviceType> d_radbasename; // per element-pair radial basis code
322+
t_fparams d_lambda, d_cut; // radial scaling lambda and cutoff, [nelements][nelements]
323+
Kokkos::View<KK_FLOAT*****, DeviceType> d_crad; // crad coeffs [nelements][nelements][nradmax][lmax+1][nradbase]
324+
309325
// Spherical Harmonics
310326

311327
void pre_compute_harmonics(int);

0 commit comments

Comments
 (0)