| Issue |
207883
|
| Summary |
[AMDGPU] Miscompiled post-barrier LDS reload in a large-workgroup HIP kernel on gfx11 (silent wrong results)
|
| Labels |
|
| Assignees |
|
| Reporter |
zpengmei
|
# [AMDGPU] Miscompiled post-barrier LDS reload in a large-workgroup HIP kernel on gfx11 (silent wrong results)
## Summary
On `gfx1100` (RDNA3), a value written to LDS (`__shared__`) by one lane, published
with `__syncthreads()`, and then reloaded by the other lanes of the workgroup can
be read back **inconsistently across lanes** when the workgroup exceeds ~512
threads. The reload appears to be miscompiled: different lanes proceed with
different values of a quantity that must be uniform, producing silently wrong
numerical results (no fault, no assertion).
This was found in rocSOLVER's small-matrix Jacobi eigensolver (`run_syevj`), where
a Jacobi rotation `(c, s)` is computed once by lane `tiy==0`, stored in LDS, and
reloaded by every row-parallel lane after a barrier to apply the rotation. Above
~512 threads the reloaded `(c, s)` differ between lanes, so different matrix rows
receive different rotations and the accumulated eigenvectors lose orthogonality.
## Environment
- GPU: AMD Radeon RX 7900 XT, `gfx1100` (RDNA3, wave32)
- clang / LLVM: `clang version 21.1.8`
- HIP: `7.1.52801`
- Compile: `hipcc -O3 --offload-arch=gfx1100 --gpu-max-threads-per-block=1024`
- Reproduces at both `-O2` and `-O3`.
## Reproducer
Self-contained, ~180 lines, depends only on `hip_runtime` (attached:
`llvm_repro_syevj.cpp`). It runs a faithful transcription of the rocSOLVER kernel
on a batch of ill-conditioned symmetric matrices and checks that the returned
eigenvector matrix `A` is orthogonal (`||A^T A - I|| ~ 1e-13`).
A single `-D` toggles only how the rotation `(c, s)` reaches the applying lanes:
- `BUG=1` (default): compute in lane `tiy==0`, store to `cosines_res/sines_diag`
in LDS, `__syncthreads()`, reload in every lane. ← upstream form
- `BUG=0`: compute `(c, s)` in every lane (a pure function of the same, unmodified
`Acpy` entries, so bit-identical across lanes). No LDS round-trip.
Both are numerically identical; the apply code that follows is byte-for-byte the
same. Only the LDS reload differs.
```
$ hipcc -O3 --offload-arch=gfx1100 --gpu-max-threads-per-block=1024 -DBUG=1 llvm_repro_syevj.cpp -o r_bug && ./r_bug
device=AMD Radeon RX 7900 XT arch=gfx1100 BUG=1
n=39 threads= 400 [small] ||A^T A - I|| worst=6.311e-14 corrupt=0/12 ok
n=48 threads= 576 [small] ||A^T A - I|| worst=4.389e+00 corrupt=12/12 <== MISCOMPILE
n=56 threads= 784 [small] ||A^T A - I|| worst=3.589e+00 corrupt=9/12 <== MISCOMPILE
n=58 threads= 841 [small] ||A^T A - I|| worst=1.047e-13 corrupt=0/12 ok
REPRODUCED (non-orthogonal eigenvectors)
$ hipcc -O3 --offload-arch=gfx1100 --gpu-max-threads-per-block=1024 -DBUG=0 llvm_repro_syevj.cpp -o r_fix && ./r_fix
... all rows: ||A^T A - I|| ~ 1e-13, clean
```
Notes:
- `n=39` → 400 threads (< 512): clean. The corruption only appears once the
workgroup crosses ~512 threads (16 wave32 waves), independent of the matrix size
(the algorithm itself is correct — `BUG=0` is clean at the same sizes).
- The failure is data dependent and, for thread counts that do not pack into whole
wavefronts (e.g. n=58 → 841 threads), non-deterministic run to run. `n=48`
(576 threads = 18 whole waves) reproduces deterministically at 12/12; a given
seed may or may not trip `n=58`. Run a few times / vary the seed to see the
intermittent sizes.
## What was ruled out (rocSOLVER-side investigation)
- Not a memory-model / visibility race: inserting `__threadfence()` before every
barrier is a no-op (bit-identical output).
- Not compiler load/store reordering across the barrier at source level: an
`asm volatile("" ::: "memory")` clobber before the apply has no effect.
- Not a broken `s_barrier`: launching extra no-op lanes past 512 threads (which
hit the barriers but do no writes) stays clean.
- Not register spilling: the kernel does not spill (VGPRs 42, ScratchSize 0).
- Not wave-size selection: rebuilding the kernel `-mwavefrontsize64` does not fix
it (if anything, more failures).
- Not `-O3`-specific: reproduces at `-O2` as well.
- Not inlining scope: moving the apply into a `__noinline__` device function does
not fix it.
- The identical row-parallel global read-modify-write pattern, in isolation
(a small standalone kernel without the surrounding convergence/norm code), is
bit-exact at 841 threads — so the write pattern and the hardware are fine. The
defect only appears in the full kernel context, and is removed by eliminating
the post-barrier LDS reload.
Taken together this points to a codegen defect around the LDS reload of a
barrier-published value under this kernel's control flow / occupancy on gfx11,
rather than an application bug (the two forms are numerically equivalent).
## Ask
- Confirmation of the miscompile and, if possible, identification of the pass /
condition (the ~512-thread threshold and whole-wave-vs-partial-wave determinism
split suggest a scheduling/occupancy interaction).
- Happy to provide LLVM IR / `-save-temps`, an `llvm-reduce`-minimized case, or
ISA on request.
## Downstream
Fixed downstream in rocSOLVER by computing the rotation per-lane instead of
broadcasting it through LDS (equivalent to `BUG=0` above): ROCm/rocm-libraries#9135. That restores correct, deterministic results at no
measurable cost.
<details>
<summary>Reproducer — <code>llvm_repro_syevj.cpp</code></summary>
```cpp
// Self-contained reproducer for an LLVM AMDGPU miscompile on gfx11 (RDNA3).
//
// hipcc -O3 --offload-arch=gfx1100 --gpu-max-threads-per-block=1024 llvm_repro_syevj.cpp -o r && ./r
//
// This is a faithful, dependency-free extraction of rocSOLVER's small-matrix Jacobi
// eigensolver device function (run_syevj / syevj_small_kernel). It diagonalizes a batch
// of symmetric matrices; the accumulated eigenvector matrix A must come out orthogonal
// (A^T A = I). Build with -DBUG=1 (default) to use the upstream form, where the rotation
// (c,s) is produced by the tiy==0 lane, stored in LDS, and reloaded by every row-parallel
// lane after __syncthreads(). Build with -DBUG=0 to compute (c,s) per lane instead.
//
// On gfx1100 the -DBUG=1 build returns non-orthogonal eigenvectors (||A^T A - I|| = O(1),
// silently) once the 2D thread array exceeds ~512 threads (n >= ~40); -DBUG=0 is clean.
// The two forms are numerically identical (same (c,s), a pure function of unmodified Acpy
// entries), so the only difference is the post-barrier LDS reload -> a codegen defect.
#include <hip/hip_runtime.h>
#include <vector>
#include <random>
#include <cmath>
#include <cstdio>
#ifndef BUG
#define BUG 1
#endif
static __device__ void givens(double f, double g, double& c, double& s, double& r){
if(g==0){ c=1; s=0; r=f; } else if(f==0){ c=0; s=1; r=g; }
else { r=std::hypot(f,g); double d=1.0/r; c=std::fabs(f)*d;
s=(f>=0? g*d : -g*d); if(f<0) r=-r; }
}
// Faithful transcription of run_syevj (evect=vector, uplo=upper, esort=none).
__global__ void __launch_bounds__(1024)
run_syevj_k(int n, double* AA, double* AcpyA, double* WW, int lda, double abstol,
int max_sweeps, int forced_ddy)
{
int tid=threadIdx.x, bid=blockIdx.z;
int even_n=n+n%2, half_n=even_n/2;
double* A = AA + (size_t)bid*lda*n;
double* Acpy= AcpyA + (size_t)bid*n*n;
double* W = WW + (size_t)bid*n;
// syevj_get_dims(n, 1024): ddy=min(256,half_n), ddx=min(1024/ddy,half_n); forced_ddy overrides ddy.
int ddy = forced_ddy>0 ? forced_ddy : (256<half_n?256:half_n);
int ddx = (1024/ddy)<half_n ? (1024/ddy) : half_n;
extern __shared__ double lmem[];
double* cosines_res=lmem; double* sines_diag=cosines_res+ddx;
int* top=(int*)(sines_diag+ddx); int* bottom=top+half_n;
int tix=tid/ddy, tiy=tid%ddy;
int dimx=ddx, dimy=ddy;
double c,s,r,f,g,mag,aij,s1,s2,temp1,temp2,local_res=0,local_diag=0;
int i,j,sweeps=0;
if(tiy==0){
for(i=tix;i<n;i+=dimx){
aij=A[i+i*lda]; local_diag+=aij*aij; Acpy[i+i*n]=aij; A[i+i*lda]=1;
for(j=n-1;j>i;j--){ aij=A[i+j*lda]; local_res+=2*aij*aij; Acpy[i+j*n]=aij; Acpy[j+i*n]=aij;
A[i+j*lda]=0; A[j+i*lda]=0; }
}
cosines_res[tix]=local_res; sines_diag[tix]=local_diag;
for(i=tix;i<half_n;i+=dimx){ top[i]=i*2; bottom[i]=i*2+1; }
}
__syncthreads();
local_res=0; local_diag=0;
for(i=0;i<dimx;i++){ local_res+=cosines_res[i]; local_diag+=sines_diag[i]; }
double tolerance=(local_res+local_diag)*abstol*abstol;
double small_num=1e-300;
int count=(half_n-1)/dimx+1;
while(sweeps<max_sweeps && local_res>tolerance){
for(int k=0;k<even_n-1;++k){
for(int cc=0;cc<count;++cc){
int kx=tix+cc*dimx;
i=kx<half_n?top[kx]:n; j=kx<half_n?bottom[kx]:n;
#if BUG
if(tiy==0 && i<n && j<n){
aij=Acpy[i+j*n]; mag=std::fabs(aij);
if(mag*mag<small_num){ c=1; s1=0; }
else { g=2*mag; f=Acpy[j+j*n]-Acpy[i+i*n];
f += (f<0)? -std::hypot(f,g): std::hypot(f,g); givens(f,g,c,s,r); s1=s*aij/mag; }
cosines_res[tix]=c; sines_diag[tix]=s1;
}
__syncthreads();
if(i<n && j<n){ c=cosines_res[tix]; s1=sines_diag[tix]; s2=s1;
#else
if(i<n && j<n){
aij=Acpy[i+j*n]; mag=std::fabs(aij);
if(mag*mag<small_num){ c=1; s1=0; }
else { g=2*mag; f=Acpy[j+j*n]-Acpy[i+i*n];
f += (f<0)? -std::hypot(f,g): std::hypot(f,g); givens(f,g,c,s,r); s1=s*aij/mag; }
}
__syncthreads();
if(i<n && j<n){ s2=s1;
#endif
for(int ky=tiy;ky<half_n;ky+=dimy){ int y1=ky*2,y2=y1+1;
temp1=Acpy[y1+i*n]; temp2=Acpy[y1+j*n];
Acpy[y1+i*n]=c*temp1+s2*temp2; Acpy[y1+j*n]=-s1*temp1+c*temp2;
if(y2<n){ temp1=Acpy[y2+i*n]; temp2=Acpy[y2+j*n];
Acpy[y2+i*n]=c*temp1+s2*temp2; Acpy[y2+j*n]=-s1*temp1+c*temp2; }
temp1=A[y1+i*lda]; temp2=A[y1+j*lda];
A[y1+i*lda]=c*temp1+s2*temp2; A[y1+j*lda]=-s1*temp1+c*temp2;
if(y2<n){ temp1=A[y2+i*lda]; temp2=A[y2+j*lda];
A[y2+i*lda]=c*temp1+s2*temp2; A[y2+j*lda]=-s1*temp1+c*temp2; }
}
}
__syncthreads();
if(i<n && j<n){
for(int ky=tiy;ky<half_n;ky+=dimy){ int y1=ky*2,y2=y1+1;
temp1=Acpy[i+y1*n]; temp2=Acpy[j+y1*n];
Acpy[i+y1*n]=c*temp1+s1*temp2; Acpy[j+y1*n]=-s2*temp1+c*temp2;
if(y2<n){ temp1=Acpy[i+y2*n]; temp2=Acpy[j+y2*n];
Acpy[i+y2*n]=c*temp1+s1*temp2; Acpy[j+y2*n]=-s2*temp1+c*temp2; }
}
}
__syncthreads();
if(tiy==0 && i<n && j<n){ Acpy[i+j*n]=0; Acpy[j+i*n]=0; }
__syncthreads();
if(tiy==0 && kx<half_n){
if(i>0){ if(i==2||i==even_n-1) top[kx]=i-1; else top[kx]=i+((i%2==0)?-2:2); }
if(j==2||j==even_n-1) bottom[kx]=j-1; else bottom[kx]=j+((j%2==0)?-2:2);
}
__syncthreads();
}
}
if(tiy==0){ local_res=0; for(i=tix;i<n;i+=dimx) for(j=0;j<i;j++) local_res+=2*Acpy[i+j*n]*Acpy[i+j*n];
cosines_res[tix]=local_res; }
__syncthreads();
local_res=0; for(i=0;i<dimx;i++) local_res+=cosines_res[i];
sweeps++;
}
if(tiy==0) for(i=tix;i<n;i+=dimx) W[i]=Acpy[i+i*n];
}
int main(){
hipDeviceProp_t p{}; hipGetDeviceProperties(&p,0);
printf("device=%s arch=%s BUG=%d\n\n",p.name,p.gcnArchName,BUG);
std::mt19937_64 rng(7);
int worst_fail=0;
for(int n:{39,48,56,58}){
int even_n=n+n%2, half_n=even_n/2, B=12;
std::normal_distribution<double> N(0,1);
// near-null SPD batch: A = Q diag(lam) Q^T, lam={1..1e-12, then 1e-16}
std::vector<double> A((size_t)B*n*n), q(n*n), lam(n);
for(int k=0;k<n;k++) lam[k]= k<8? std::pow(1e-12,double(k)/7):1e-16;
for(int b=0;b<B;b++){ for(double&x:q)x=N(rng);
for(int k=0;k<n;k++){ for(int pp=0;pp<k;pp++){ double d=0; for(int t=0;t<n;t++)d+=q[t+k*n]*q[t+pp*n];
for(int t=0;t<n;t++)q[t+k*n]-=d*q[t+pp*n]; }
double nr=0; for(int t=0;t<n;t++)nr+=q[t+k*n]*q[t+k*n]; nr=1/std::sqrt(nr);
for(int t=0;t<n;t++)q[t+k*n]*=nr; }
double*Ab=A.data()+(size_t)b*n*n;
for(int jj=0;jj<n;jj++)for(int ii=0;ii<n;ii++){ double a=0; for(int k=0;k<n;k++)a+=q[ii+k*n]*lam[k]*q[jj+k*n]; Ab[ii+jj*n]=a; }
}
double *dA,*dAcpy,*dW;
hipMalloc(&dA,sizeof(double)*A.size()); hipMalloc(&dAcpy,sizeof(double)*A.size()); hipMalloc(&dW,sizeof(double)*B*n);
hipMemcpy(dA,A.data(),sizeof(double)*A.size(),hipMemcpyHostToDevice);
int ddy = 256<half_n?256:half_n; int ddx=(1024/ddy)<half_n?(1024/ddy):half_n;
int threads=ddx*ddy, sh=(2*ddx)*sizeof(double)+(2*half_n)*sizeof(int);
run_syevj_k<<<dim3(1,1,B),dim3(threads),sh>>>(n,dA,dAcpy,dW,n,1e-12,100,0);
hipDeviceSynchronize();
std::vector<double> V(A.size()); hipMemcpy(V.data(),dA,sizeof(double)*A.size(),hipMemcpyDeviceToHost);
double worst=0; int nfail=0;
for(int b=0;b<B;b++){ const double*Vb=V.data()+(size_t)b*n*n; double o=0;
for(int a=0;a<n;a++)for(int cc=0;cc<n;cc++){ double d=0; for(int k=0;k<n;k++)d+=Vb[k+a*n]*Vb[k+cc*n]; d-=(a==cc); o+=d*d; }
o=std::sqrt(o); if(o>worst)worst=o; if(o>1e-8)nfail++; }
printf("n=%2d threads=%4d [%s] ||A^T A - I|| worst=%.3e corrupt=%d/%d %s\n",
n,threads, n<=58?"small":"blk", worst,nfail,B, nfail?" <== MISCOMPILE":" ok");
worst_fail+=nfail;
hipFree(dA);hipFree(dAcpy);hipFree(dW);
}
printf("\n%s\n", worst_fail? "REPRODUCED (non-orthogonal eigenvectors)":"clean");
return worst_fail?1:0;
}
```
</details>
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs