mirror of
https://github.com/paboyle/Grid.git
synced 2026-07-31 07:53:28 +01:00
Compare commits
4
Commits
1315d4604d
...
2fadd8bb62
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fadd8bb62 | ||
|
|
60df2dd5d0 | ||
|
|
66b529b345 | ||
|
|
1304172a93 |
@@ -239,43 +239,136 @@ inline typename vobj::scalar_objectD sumD_gpu_small(const vobj *lat, Integer osi
|
||||
return result;
|
||||
}
|
||||
|
||||
// Pack R consecutive vector_type words of lat[0..osites-1] starting at word
|
||||
// 'base' into a Bundle = iVector<iScalar<vector>,R> per site, then reduce
|
||||
// with sumD_gpu_small. Bundle::Nsimd() == vector::Nsimd(), so the existing
|
||||
// shared-memory kernel handles SIMD-lane extraction and double-promotion
|
||||
// correctly. sizeof(Bundle::scalar_objectD) = R*sizeof(scalarD) <= 192 B
|
||||
// for R<=12, safely within sharedMemPerBlock on all supported devices.
|
||||
// Fused pack+reduce: reads R words of each vobj at word offset 'base',
|
||||
// accumulates directly into iVector<iScalar<scalarD>,R> without staging
|
||||
// through an intermediate bundle buffer. One HBM pass instead of three.
|
||||
template <int R, class vobj, class sobj, class Iterator>
|
||||
__device__ void packReduceBlocks(
|
||||
const iScalar<typename vobj::vector_type> *idat,
|
||||
sobj *g_odata, Iterator osites, int base, int words)
|
||||
{
|
||||
constexpr Iterator nsimd = vobj::Nsimd();
|
||||
Iterator blockSize = blockDim.x;
|
||||
|
||||
extern __shared__ __align__(COALESCE_GRANULARITY) unsigned char shmem_pointer[];
|
||||
sobj *sdata = (sobj *)shmem_pointer;
|
||||
|
||||
Iterator tid = threadIdx.x;
|
||||
Iterator i = blockIdx.x * (blockSize * 2) + threadIdx.x;
|
||||
Iterator gridSize = blockSize * 2 * gridDim.x;
|
||||
sobj mySum = Zero();
|
||||
|
||||
while (i < osites * nsimd) {
|
||||
Iterator lane = i % nsimd;
|
||||
Iterator ss = i / nsimd;
|
||||
sobj tmpD; zeroit(tmpD);
|
||||
for (int k = 0; k < R; k++) {
|
||||
auto w = extractLane(lane, idat[ss * words + base + k]);
|
||||
iScalar<typename vobj::scalar_typeD> wd; wd = w;
|
||||
tmpD._internal[k] = wd;
|
||||
}
|
||||
mySum += tmpD;
|
||||
|
||||
if (i + blockSize < osites * nsimd) {
|
||||
lane = (i + blockSize) % nsimd;
|
||||
ss = (i + blockSize) / nsimd;
|
||||
sobj tmpD2; zeroit(tmpD2);
|
||||
for (int k = 0; k < R; k++) {
|
||||
auto w = extractLane(lane, idat[ss * words + base + k]);
|
||||
iScalar<typename vobj::scalar_typeD> wd; wd = w;
|
||||
tmpD2._internal[k] = wd;
|
||||
}
|
||||
mySum += tmpD2;
|
||||
}
|
||||
i += gridSize;
|
||||
}
|
||||
|
||||
reduceBlock(sdata, mySum, tid);
|
||||
if (tid == 0) g_odata[blockIdx.x] = sdata[0];
|
||||
}
|
||||
|
||||
template <int R, class vobj, class sobj, class Iterator>
|
||||
__global__ void packReduceKernel(
|
||||
const iScalar<typename vobj::vector_type> *idat,
|
||||
sobj *buffer, Iterator osites, int base, int words)
|
||||
{
|
||||
Iterator blockSize = blockDim.x;
|
||||
|
||||
packReduceBlocks<R, vobj, sobj>(idat, buffer, osites, base, words);
|
||||
|
||||
if (gridDim.x > 1) {
|
||||
const Iterator tid = threadIdx.x;
|
||||
__shared__ bool amLast;
|
||||
extern __shared__ __align__(COALESCE_GRANULARITY) unsigned char shmem_pointer[];
|
||||
sobj *smem = (sobj *)shmem_pointer;
|
||||
|
||||
acceleratorFence();
|
||||
|
||||
if (tid == 0) {
|
||||
unsigned int ticket = atomicInc(&retirementCount, gridDim.x);
|
||||
amLast = (ticket == gridDim.x - 1);
|
||||
}
|
||||
acceleratorSynchroniseAll();
|
||||
|
||||
if (amLast) {
|
||||
Iterator i = tid;
|
||||
sobj mySum = Zero();
|
||||
while (i < (Iterator)gridDim.x) {
|
||||
mySum += buffer[i];
|
||||
i += blockSize;
|
||||
}
|
||||
reduceBlock(smem, mySum, tid);
|
||||
if (tid == 0) {
|
||||
buffer[0] = smem[0];
|
||||
retirementCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<int R, class vobj>
|
||||
inline void sumD_gpu_reduce_words(const vobj *lat, Integer osites,
|
||||
typename vobj::scalar_typeD *ret_p, int base)
|
||||
{
|
||||
typedef typename vobj::vector_type vector;
|
||||
using Bundle = iVector<iScalar<vector>, R>;
|
||||
typedef typename vobj::vector_type vector;
|
||||
typedef typename vobj::scalar_typeD scalarD;
|
||||
using BundleScalarD = iVector<iScalar<scalarD>, R>;
|
||||
|
||||
constexpr int Nsimd = vobj::Nsimd();
|
||||
const int words = sizeof(vobj) / sizeof(vector);
|
||||
iScalar<vector> *idat = (iScalar<vector> *)lat;
|
||||
const iScalar<vector> *idat = (const iScalar<vector> *)lat;
|
||||
|
||||
deviceVector<Bundle> buf(osites);
|
||||
Bundle *buf_p = &buf[0];
|
||||
Integer size = (Integer)osites * Nsimd;
|
||||
Integer numThreads, numBlocks;
|
||||
int ok = getNumBlocksAndThreads(size, sizeof(BundleScalarD), numThreads, numBlocks);
|
||||
GRID_ASSERT(ok);
|
||||
|
||||
Integer smemSize = numThreads * sizeof(BundleScalarD);
|
||||
deviceVector<BundleScalarD> buffer(numBlocks);
|
||||
BundleScalarD *buffer_v = &buffer[0];
|
||||
BundleScalarD result;
|
||||
|
||||
#ifdef GRID_REDUCTION_TIMING
|
||||
RealD t_pack = -usecond();
|
||||
RealD t_kernel = -usecond();
|
||||
#endif
|
||||
accelerator_for(ss, osites, 1, {
|
||||
Bundle b;
|
||||
for (int k = 0; k < R; k++)
|
||||
b._internal[k] = idat[ss * words + base + k];
|
||||
buf_p[ss] = b;
|
||||
});
|
||||
packReduceKernel<R, vobj, BundleScalarD, Integer>
|
||||
<<<numBlocks, numThreads, smemSize, computeStream>>>
|
||||
(idat, buffer_v, osites, base, words);
|
||||
accelerator_barrier();
|
||||
#ifdef GRID_REDUCTION_TIMING
|
||||
t_pack += usecond();
|
||||
t_kernel += usecond();
|
||||
RealD t_d2h = -usecond();
|
||||
#endif
|
||||
acceleratorCopyFromDevice(buffer_v, &result, sizeof(result));
|
||||
#ifdef GRID_REDUCTION_TIMING
|
||||
t_d2h += usecond();
|
||||
std::cout << GridLogMessage << " sumD_gpu_reduce_words R=" << R
|
||||
<< " base=" << base << " pack=" << t_pack << " us" << std::endl;
|
||||
<< " base=" << base
|
||||
<< " kernel=" << t_kernel << " D2H=" << t_d2h << " us" << std::endl;
|
||||
#endif
|
||||
|
||||
auto sum_bundle = sumD_gpu_small(buf_p, osites);
|
||||
for (int k = 0; k < R; k++)
|
||||
ret_p[base + k] = TensorRemove(sum_bundle._internal[k]);
|
||||
ret_p[base + k] = TensorRemove(result._internal[k]);
|
||||
}
|
||||
|
||||
template <class vobj>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
NAMESPACE_BEGIN(Grid);
|
||||
int world_rank; // Use to control world rank for print guarding
|
||||
int acceleratorAbortOnGpuError=1;
|
||||
uint32_t accelerator_threads=2;
|
||||
uint32_t accelerator_threads=16;
|
||||
uint32_t acceleratorThreads(void) {return accelerator_threads;};
|
||||
void acceleratorThreads(uint32_t t) {accelerator_threads = t;};
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
name: gpu-memory-performance
|
||||
description: Diagnose and fix GPU memory bandwidth and occupancy problems in Grid HPC kernels — acceleratorThreads() pitfalls, LambdaApply thread mapping, coalescedRead/Write idiom, when to use accelerator_for vs a hand-rolled __global__ kernel, and fused vs staged HBM access patterns.
|
||||
user-invocable: true
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Bash(grep -r)
|
||||
---
|
||||
|
||||
# GPU Memory Performance in Grid
|
||||
|
||||
## The acceleratorThreads() Trap
|
||||
|
||||
`acceleratorThreads()` is a runtime-settable global (default **2**) that controls the `blockDim.y` of every `accelerator_for` launch. It is NOT the SIMD width — it is the number of sites processed per block in the y-dimension.
|
||||
|
||||
```cpp
|
||||
// Grid/threads/Accelerator.cc
|
||||
uint32_t accelerator_threads = 2; // <-- default
|
||||
```
|
||||
|
||||
With `accelerator_for(ss, osites, nsimd, ...)`, the launch is:
|
||||
|
||||
```
|
||||
dim3 threads(nsimd, acceleratorThreads(), 1)
|
||||
dim3 blocks ((osites + acceleratorThreads() - 1) / acceleratorThreads(), 1, 1)
|
||||
```
|
||||
|
||||
For `nsimd=1` and the default `acceleratorThreads()=2`:
|
||||
- **2 threads per block** on a 64-thread AMD wavefront → **3% occupancy**
|
||||
- Expected bandwidth ≈ peak × 3% ≈ 50 GB/s on MI250X
|
||||
|
||||
**Diagnostic**: observed bandwidth << peak, kernel time >> expected from data volume. Check with `--accelerator-threads 16` or `--accelerator-threads 32` at runtime. A large speedup confirms occupancy starvation.
|
||||
|
||||
**Fix options** (in order of preference):
|
||||
1. Kernel needs its own thread count — use `getNumBlocksAndThreads` and launch a `__global__` kernel directly (see below).
|
||||
2. Temporarily acceptable: set `--accelerator-threads 16` or 32 at the application level. Note this affects every `accelerator_for` site in the binary.
|
||||
|
||||
## LambdaApply Thread Mapping
|
||||
|
||||
`accelerator_for` and `accelerator_for2d` go through `LambdaApply`:
|
||||
|
||||
```cpp
|
||||
// HIP/CUDA LambdaApply kernel:
|
||||
uint64_t x = threadIdx.y + blockDim.y * blockIdx.x; // iter1 (site index)
|
||||
uint64_t y = threadIdx.z + blockDim.z * blockIdx.y; // iter2
|
||||
uint64_t z = threadIdx.x; // lane (SIMD lane)
|
||||
Lambda(x, y, z);
|
||||
```
|
||||
|
||||
`threadIdx.x` is the **fast** (lane) dimension — consecutive thread IDs within a warp/wavefront correspond to consecutive lane values on the **same** site, not consecutive sites.
|
||||
|
||||
Consequence: for coalesced access from a `vobj` array (AoS layout, stride = `sizeof(vobj)` between adjacent sites), adjacent threads in a wavefront address the **same** site at different lanes, not adjacent sites. With `Nsimd=1` (GPU scalar build), `threadIdx.x` is always 0 and provides no coalescing benefit at all.
|
||||
|
||||
## coalescedRead / coalescedWrite
|
||||
|
||||
These are Grid's canonical way to read/write one SIMD lane from a vector type inside a `GRID_SIMT` kernel:
|
||||
|
||||
```cpp
|
||||
// accelerator_for(ss, osites, Nsimd, {
|
||||
// lane = acceleratorSIMTlane(Nsimd) = threadIdx.x
|
||||
auto scalar_val = coalescedRead(field[ss]); // extractLane(lane, field[ss])
|
||||
coalescedWrite(field[ss], scalar_val); // insertLane(lane, field[ss], scalar_val)
|
||||
```
|
||||
|
||||
For `vobj` aggregate types, `coalescedRead` calls `extractLane(lane, vobj)` which recurses through the tensor hierarchy and returns `vobj::scalar_object`.
|
||||
|
||||
For `vsimd` (raw SIMD vector) types, it casts to `scalar_type*` and indexes with `lane`.
|
||||
|
||||
**When Nsimd=1** (GPU scalar build): `lane=0` always, so `coalescedRead`/`coalescedWrite` are effectively no-ops (direct read/write). Coalescing must be achieved through the iteration structure instead.
|
||||
|
||||
## Coalescing the Iteration Structure
|
||||
|
||||
For an AoS input array where each site is `words` 16-byte elements, adjacent threads reading the same site's consecutive words achieve coalesced access:
|
||||
|
||||
```cpp
|
||||
// Good: k varies across threads in a block → consecutive 16-byte reads
|
||||
accelerator_for2d(k, R, ss, osites, Nsimd, {
|
||||
coalescedWrite(out[ss]._internal[k],
|
||||
coalescedRead(idat[ss * words + base + k]));
|
||||
});
|
||||
// dim3(Nsimd, nt, 1): threadIdx.y = k (consecutive words, coalesced)
|
||||
// threadIdx.x = lane (SIMD sub-lane, coalesced for Nsimd>1)
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Bad: each thread reads all R words of its site serially
|
||||
accelerator_for(ss, osites, 1, {
|
||||
Bundle b;
|
||||
for (int k = 0; k < R; k++)
|
||||
b._internal[k] = idat[ss * words + base + k]; // serial, not coalesced across threads
|
||||
out[ss] = b; // bulk struct write
|
||||
});
|
||||
```
|
||||
|
||||
The bad pattern also accumulates a large struct in registers (192 bytes for R=12), increasing register pressure and reducing occupancy further.
|
||||
|
||||
## When to Use a __global__ Kernel Instead of accelerator_for
|
||||
|
||||
`accelerator_for` is correct for site-parallel work where `acceleratorThreads()` is tuned appropriately. Use a direct `__global__` kernel when:
|
||||
|
||||
- The kernel requires a **specific thread count** for correctness or performance (reductions, shared-memory algorithms).
|
||||
- The optimal thread count depends on `sizeof(sobj)` and `sharedMemPerBlock`, not on a runtime global.
|
||||
- You need the retirement-count pattern for cross-block final reduction.
|
||||
|
||||
Pattern: use `getNumBlocksAndThreads` to pick `numThreads` and `numBlocks`:
|
||||
|
||||
```cpp
|
||||
Integer numThreads, numBlocks;
|
||||
int ok = getNumBlocksAndThreads(n, sizeof(sobj), numThreads, numBlocks);
|
||||
// starts at warpSize (32/64), doubles while 2*threads*sizeof(sobj) < sharedMemPerBlock
|
||||
// gives 64–256 threads/block → near-100% wavefront occupancy
|
||||
Integer smemSize = numThreads * sizeof(sobj);
|
||||
myKernel<<<numBlocks, numThreads, smemSize, computeStream>>>(args...);
|
||||
```
|
||||
|
||||
This gives 64–256 threads/block regardless of `acceleratorThreads()`. Grid's `reduceKernel` uses this pattern and achieves ~400 GB/s on MI250X.
|
||||
|
||||
## Fused vs Staged HBM Access
|
||||
|
||||
A staged pack+reduce reads the data **three times**:
|
||||
|
||||
```
|
||||
pack kernel: reads vobj array (N bytes), writes bundle buffer (N bytes)
|
||||
reduce kernel: reads bundle buffer (N bytes), writes tiny result buffer
|
||||
```
|
||||
|
||||
Total HBM: 3N bytes for N bytes of useful input.
|
||||
|
||||
A fused kernel reads the data **once**:
|
||||
|
||||
```
|
||||
packReduceKernel: reads R words of vobj array (N bytes), reduces in-place
|
||||
```
|
||||
|
||||
Total HBM: N bytes. Register pressure increases (R words held per thread) but the 3× HBM saving dominates for large objects.
|
||||
|
||||
The fused pattern in Grid's `sumD_gpu_reduce_words<R>`:
|
||||
|
||||
```cpp
|
||||
template <int R, class vobj, class sobj, class Iterator>
|
||||
__device__ void packReduceBlocks(
|
||||
const iScalar<typename vobj::vector_type> *idat,
|
||||
sobj *g_odata, Iterator osites, int base, int words)
|
||||
{
|
||||
// sobj = iVector<iScalar<scalarD>, R> (R double-precision scalars per site)
|
||||
constexpr Iterator nsimd = vobj::Nsimd();
|
||||
...
|
||||
while (i < osites * nsimd) {
|
||||
Iterator lane = i % nsimd;
|
||||
Iterator ss = i / nsimd;
|
||||
sobj tmpD; zeroit(tmpD);
|
||||
for (int k = 0; k < R; k++) {
|
||||
auto w = extractLane(lane, idat[ss * words + base + k]);
|
||||
iScalar<typename vobj::scalar_typeD> wd; wd = w; // float→double promotion
|
||||
tmpD._internal[k] = wd;
|
||||
}
|
||||
mySum += tmpD;
|
||||
...
|
||||
}
|
||||
reduceBlock(sdata, mySum, tid);
|
||||
}
|
||||
```
|
||||
|
||||
Launched with `getNumBlocksAndThreads` → 128–256 threads/block → correct occupancy without depending on `acceleratorThreads()`.
|
||||
|
||||
## Observed Numbers on MI250X (32^4 LatticePropagatorD, Nsimd=1)
|
||||
|
||||
| Configuration | pack µs/group | reduce µs/group | total µs | GB/s |
|
||||
|---|---|---|---|---|
|
||||
| acceleratorThreads=2, staged | 10,080 | 470 | 126,909 | 50 |
|
||||
| acceleratorThreads=16, staged | 342 | 310 | 8,251 | 297 |
|
||||
| acceleratorThreads=16, fused | — | 349 | 4,584 | 546 |
|
||||
|
||||
The fused kernel at 349 µs/group reads 201 MB at 576 GB/s — 36% of MI250X HBM peak. The remaining gap from peak is the in-kernel serial loop over R=12 words and the 12 serial kernel launches.
|
||||
|
||||
## Quick Checklist When a Kernel Is Slow
|
||||
|
||||
1. Check threads per block: `accelerator_for(ss, N, 1, ...)` with default `acceleratorThreads()=2` = 2 threads/block = 3% occupancy on AMD. Try `--accelerator-threads 16` at runtime; if it helps a lot, occupancy is the problem.
|
||||
2. Check for bulk struct accumulation in registers (`Bundle b; for(...) b._internal[k] = ...;`). Replace with per-element writes via `coalescedWrite`.
|
||||
3. Check for staged HBM access (pack → buffer → reduce). Count the passes; fuse if ≥ 2 passes over the same data.
|
||||
4. For reduction kernels, always use `getNumBlocksAndThreads` rather than `accelerator_for` so thread count is independent of `acceleratorThreads()`.
|
||||
Reference in New Issue
Block a user