Debug ulimit as core files driving me crazy

This commit is contained in:
Peter Boyle
2026-08-28 11:20:43 -04:00
parent 9b4c388585
commit 63c2cdb712
4 changed files with 66 additions and 6 deletions
+12 -4
View File
@@ -160,7 +160,7 @@ void Run(GridCartesian *grid, const char *tname, uint64_t nbytes_lo, uint64_t nb
void Scalar(GridCartesian *grid, int reps, double gap_us) void Scalar(GridCartesian *grid, int reps, double gap_us)
{ {
int me=grid->ThisRank(); int me=grid->ThisRank();
Stats<RealD> sP2P, sBare, sVec, sCart, sFlat, sP2Pdirect; Stats<RealD> sP2P, sBare, sVec, sCart, sFlat, sP2Pdirect, sP2Phead;
RealD x; deviceVector<RealD> d(1); std::vector<RealD> h(1); RealD x; deviceVector<RealD> d(1); std::vector<RealD> h(1);
// gap: a bandwidth-bound device kernel sized to take ~gap_us (calibrated once) // gap: a bandwidth-bound device kernel sized to take ~gap_us (calibrated once)
uint64_t gapN = 0; deviceVector<RealD> gapbuf(1); uint64_t gapN = 0; deviceVector<RealD> gapbuf(1);
@@ -179,7 +179,14 @@ void Scalar(GridCartesian *grid, int reps, double gap_us)
// A Barrier before each timed call so that node skew (from the gap kernel or // A Barrier before each timed call so that node skew (from the gap kernel or
// anything else) is not charged to the reduction: the timer measures the // anything else) is not charged to the reduction: the timer measures the
// collective from a synchronised start. // collective from a synchronised start.
// The same GlobalSum is timed twice per rep: first thing after the previous
// rep's flat ring (574 Sendrecvs per rank at P=288, staggered exit), and again
// after the MPI calls. 2026-08-28 at P=288: first-position mean 198 us vs 51 us
// for the identical primitive one line later -- position, not the wrapper
// (StepLog is a pointer assignment). If "head" stays slow and "P2P" matches
// the direct call, MPI is draining the ring's backlog into the next P2P call.
for(int r=0;r<reps;r++){ for(int r=0;r<reps;r++){
gap(); x=1.0+me; grid->Barrier(); { double t0=usecond(); grid->GlobalSum(x); sP2Phead.add(usecond()-t0); }
gap(); x=1.0+me; grid->Barrier(); { double t0=usecond(); grid->GlobalSum(x); sP2P.add(usecond()-t0); } gap(); x=1.0+me; grid->Barrier(); { double t0=usecond(); grid->GlobalSum(x); sP2P.add(usecond()-t0); }
gap(); x=1.0+me; grid->Barrier(); { double t0=usecond(); grid->GlobalSumP2P(x); sP2Pdirect.add(usecond()-t0); } gap(); x=1.0+me; grid->Barrier(); { double t0=usecond(); grid->GlobalSumP2P(x); sP2Pdirect.add(usecond()-t0); }
gap(); h[0]=1.0+me;grid->Barrier(); { double t0=usecond(); BareAllreduce(grid,&h[0],1); sBare.add(usecond()-t0); } gap(); h[0]=1.0+me;grid->Barrier(); { double t0=usecond(); BareAllreduce(grid,&h[0],1); sBare.add(usecond()-t0); }
@@ -190,11 +197,12 @@ void Scalar(GridCartesian *grid, int reps, double gap_us)
auto mx=[&](double v){ RealD y=v; grid->GlobalMax(y); return (double)y; }; auto mx=[&](double v){ RealD y=v; grid->GlobalMax(y); return (double)y; };
auto line=[&](const char *nm, Stats<RealD> &st){ auto line=[&](const char *nm, Stats<RealD> &st){
double tmin=mx(st.tmin), tmean=mx(st.tsum/st.n); double tmin=mx(st.tmin), tmean=mx(st.tsum/st.n);
if ( me==0 ) std::cout << GridLogMessage << " " << std::setw(34) << std::left << nm << std::right if ( me==0 ) std::cout << GridLogMessage << " " << std::setw(42) << std::left << nm << std::right
<< " min " << std::setw(8) << tmin << " us mean " << std::setw(8) << tmean << " us" << std::endl; }; << " min " << std::setw(8) << tmin << " us mean " << std::setw(8) << tmean << " us" << std::endl; };
if ( me==0 ) std::cout << GridLogMessage << "==== SCALAR latency (RealD, " << reps << " reps, slowest rank, Barrier before each timed call, gap " << gap_us << " us) P=" << grid->ProcessorCount() << std::endl; if ( me==0 ) std::cout << GridLogMessage << "==== SCALAR latency (RealD, " << reps << " reps, slowest rank, Barrier before each timed call, gap " << gap_us << " us) P=" << grid->ProcessorCount() << std::endl;
line("GlobalSum(RealD) = GlobalSumP2P", sP2P); line("GlobalSum(RealD) [head: 1st after flat ring]", sP2Phead);
line("GlobalSum(RealD) = GlobalSumP2Pdirect", sP2Pdirect); line("GlobalSum(RealD) = GlobalSumP2P [2nd]", sP2P);
line("GlobalSumP2P direct [3rd]", sP2Pdirect);
line("bare MPI_Allreduce(1 double)", sBare); line("bare MPI_Allreduce(1 double)", sBare);
line("GlobalSumVector(double*,1) [MPI]", sVec); line("GlobalSumVector(double*,1) [MPI]", sVec);
line("CartesianRingAllReduce n=1 (device)",sCart); line("CartesianRingAllReduce n=1 (device)",sCart);
+30
View File
@@ -99,3 +99,33 @@ Grid uses `computeStream` (CUDA/HIP) or `theGridAccelerator` (SYCL) consistently
3. If results are non-deterministic across runs, insert a second barrier and observe whether reproducibility improves. 3. If results are non-deterministic across runs, insert a second barrier and observe whether reproducibility improves.
4. For correctness-critical operations (reductions that will be compared against reference values), add the double-run checksum test from `correctness-verification.md`. 4. For correctness-critical operations (reductions that will be compared against reference values), add the double-run checksum test from `correctness-verification.md`.
5. If the process hangs at 100% CPU in a runtime library function, this is a driver/runtime bug — there is no application-level fix beyond scheduling a node reboot. 5. If the process hangs at 100% CPU in a runtime library function, this is a driver/runtime bug — there is no application-level fix beyond scheduling a node reboot.
## Making an asynchronous GPU fault attributable
A "Memory access fault by GPU node-N ... Reason: Unknown" is raised by the
runtime, asynchronously, possibly long after the offending kernel or copy
was *queued*. `--debug-signals` sees nothing useful: the host is elsewhere
by then. Before exhaustive logging or bisection, make the fault land on the
call that caused it:
| runtime | serialise launches/copies | name kernels as they launch |
|---|---|---|
| HIP/ROCm | `AMD_SERIALIZE_KERNEL=3 AMD_SERIALIZE_COPY=3` (every launch and copy synchronous) | `AMD_LOG_LEVEL=3` (4 is very verbose) |
| CUDA | `CUDA_LAUNCH_BLOCKING=1` | `compute-sanitizer --tool memcheck ./bin ...` (names the kernel and the bad address) |
| SYCL/L0 | `SYCL_PI_TRACE=2` / `ZE_DEBUG=1` | (Level Zero validation layer: `ZE_ENABLE_VALIDATION_LAYER=1`) |
5-10x slower, so run on the smallest reproducer (one node, small lattice).
The last kernel name printed before the fault is the culprit; combined with
Grid's `--log ...,Memory` (every MemoryManager transfer with size, direction,
device and host pointers, in program order) it separates "stale pointer after
eviction" from "out-of-range index" in one run. First used 2026-08-28 for the
NRHS>=6 fault in the 3-level example (`systems/Frontier/nrhs_fault.job`).
**GPU core dumps (ROCm):** on a fault the runtime writes `gpucore.<pid>` (a
GCD's memory, ~22 GB on MI250X) into the process's working directory; it obeys
`ulimit -c` exactly as the kernel core does, so `ulimit -c 0` in the batch
script suppresses it (Slurm propagates rlimits to `srun` tasks by default) and
`ulimit -c unlimited` + `HSA_ENABLE_DEBUG=1` produces one ROCgdb can load.
Deleted-while-open dumps on NFS persist as hidden `.nfs*` files against quota
until the holder exits. Run fault hunts from a Lustre directory.
Source: ROCgdb documentation, "AMD GPU" chapter, core-dump section.
+12 -1
View File
@@ -32,6 +32,13 @@
# dense bottom or MemoryManager eviction is involved. # dense bottom or MemoryManager eviction is involved.
############################################################################## ##############################################################################
# Run in a per-job subdirectory on Lustre: logs, per-rank --debug-stdout trees and GPU
# core files are large and must not land in $HOME (quota). Everything below is
# relative to RUNDIR; the job's own slurm-*.out stays where sbatch was run.
LUSTRE=/lustre/orion/phy157/proj-shared/phy157_dwf/paboyle
RUNDIR=$LUSTRE/runs/${SLURM_JOB_NAME}_${SLURM_JOB_ID}
mkdir -p $RUNDIR && cd $RUNDIR && echo "RUNDIR $RUNDIR"
cat << EOF > select_gpu cat << EOF > select_gpu
#!/bin/bash #!/bin/bash
export GPU_MAP=(0 1 2 3 7 6 5 4) export GPU_MAP=(0 1 2 3 7 6 5 4)
@@ -49,6 +56,10 @@ root=$HOME/ParallelIO/systems/Frontier
source $root/sourceme-rocm7.2.sh source $root/sourceme-rocm7.2.sh
export OMP_NUM_THREADS=7 export OMP_NUM_THREADS=7
# No core dumps: the ROCm runtime's GPU core dump (gpucore.<pid>, ~22 GB per faulting GCD, the
# .nfs quota disaster of 2026-08-25) obeys ulimit -c like the kernel's (ROCgdb docs, "AMD GPU
# core dumps"); Slurm propagates the batch shell's limits to srun tasks by default.
ulimit -c 0
export MPICH_GPU_SUPPORT_ENABLED=1 export MPICH_GPU_SUPPORT_ENABLED=1
export MPICH_SMP_SINGLE_COPY_MODE=CMA export MPICH_SMP_SINGLE_COPY_MODE=CMA
export MPICH_OFI_NIC_POLICY=GPU export MPICH_OFI_NIC_POLICY=GPU
@@ -70,7 +81,7 @@ export LATT=$vol
export LS=24 export LS=24
# The subspace is generated on first use and saved to SUBSPACE_FILE (a few minutes). # The subspace is generated on first use and saved to SUBSPACE_FILE (a few minutes).
export SUBSPACE_FILE=$PWD/subspace_16x8x24x96_nb64.scidac export SUBSPACE_FILE=$LUSTRE/subspace_16x8x24x96_nb64.scidac # persistent across jobs (generated once)
unset SLAB_FILE unset SLAB_FILE
export HOT_START=1 export HOT_START=1
export MASS=0.00078 export MASS=0.00078
+12 -1
View File
@@ -33,6 +33,13 @@
# (the last kernel name / MemoryManager line before the fault); same for fault36_B. # (the last kernel name / MemoryManager line before the fault); same for fault36_B.
############################################################################## ##############################################################################
# Run in a per-job subdirectory on Lustre: logs, per-rank --debug-stdout trees and GPU
# core files are large and must not land in $HOME (quota). Everything below is
# relative to RUNDIR; the job's own slurm-*.out stays where sbatch was run.
LUSTRE=/lustre/orion/phy157/proj-shared/phy157_dwf/paboyle
RUNDIR=$LUSTRE/runs/${SLURM_JOB_NAME}_${SLURM_JOB_ID}
mkdir -p $RUNDIR && cd $RUNDIR && echo "RUNDIR $RUNDIR"
cat << EOF > select_gpu cat << EOF > select_gpu
#!/bin/bash #!/bin/bash
export GPU_MAP=(0 1 2 3 7 6 5 4) export GPU_MAP=(0 1 2 3 7 6 5 4)
@@ -50,6 +57,10 @@ root=$HOME/ParallelIO/systems/Frontier
source $root/sourceme-rocm7.2.sh source $root/sourceme-rocm7.2.sh
export OMP_NUM_THREADS=7 export OMP_NUM_THREADS=7
# No core dumps: the ROCm runtime's GPU core dump (gpucore.<pid>, ~22 GB per faulting GCD, the
# .nfs quota disaster of 2026-08-25) obeys ulimit -c like the kernel's (ROCgdb docs, "AMD GPU
# core dumps"); Slurm propagates the batch shell's limits to srun tasks by default.
ulimit -c 0
export MPICH_GPU_SUPPORT_ENABLED=1 export MPICH_GPU_SUPPORT_ENABLED=1
export MPICH_SMP_SINGLE_COPY_MODE=CMA export MPICH_SMP_SINGLE_COPY_MODE=CMA
export MPICH_OFI_NIC_POLICY=GPU export MPICH_OFI_NIC_POLICY=GPU
@@ -86,7 +97,7 @@ export NRHS=6
run_cell () { run_cell () {
name=$1; shift name=$1; shift
echo "----- $name : $* -----" echo "----- $name : $* -----"
export GRID_STDOUT_ROOT=$PWD/fault36_$name export GRID_STDOUT_ROOT=$RUNDIR/fault36_$name
env "$@" srun -N36 -n288 --kill-on-bad-exit=1 ./select_gpu $BIN --mpi ${MPI_GEOM} --grid $vol $OPTS1 --comms-overlap \ env "$@" srun -N36 -n288 --kill-on-bad-exit=1 ./select_gpu $BIN --mpi ${MPI_GEOM} --grid $vol $OPTS1 --comms-overlap \
--debug-stdout --log Error,Warning,Message,Memory > log.fault36.$name 2>&1 --debug-stdout --log Error,Warning,Message,Memory > log.fault36.$name 2>&1
echo " exit $?"; sleep 30 echo " exit $?"; sleep 30