Skip to content

Silent wrong answers on GPU from 32 qubits upward: kernel launch failures are never checked #815

Description

@ipasichnyk

On an AMD GPU, any QuEST kernel that needs 2^32 or more threads fails to launch with hipErrorInvalidConfiguration. QuEST does not check kernel launch errors, so the operation silently does nothing, the API returns success, and the caller gets a wrong answer with no diagnostic. This begins at 32 qubits for uncontrolled diagonal operations and 33 qubits for one-target dense gates.

Environment

  • QuEST aa19534 (v4.3-release), unmodified
  • AMD Instinct MI355X (gfx950), 288 GiB, single GPU, no MPI
  • ROCm 7.2.1, HIP 7.2.53211, amdclang++, Release, QUEST_FLOAT_PRECISION=2

Reproducer

#include <cstdlib>
#include <cmath>
#include <iostream>
#include "quest.h"

// declared directly to avoid HIP headers on the host compile line
extern "C" int hipGetLastError(void);
extern "C" const char* hipGetErrorString(int);

static int failures = 0;

static void check(const char* what, qreal got, qreal want) {
    int ok = std::abs(got - want) < 1e-9;
    failures += !ok;
    std::cout << (ok ? "  pass  " : "  FAIL  ") << what
              << " = " << got << "  (expected " << want << ")" << std::endl;
}

int main(int argc, char* argv[]) {
    int n = (argc > 1) ? std::atoi(argv[1]) : 32;

    initQuESTEnv();
    Qureg q = createQureg(n);

    qindex numAmps = (qindex) 1 << n;
    qindex half    = (qindex) 1 << (n - 1);

    // [A] uncontrolled diagonal -> 2^n threads
    std::cout << "[A] applyPauliZ (" << numAmps << " threads)" << std::endl;
    initPlusState(q);
    applyPauliZ(q, 0);

    int err = hipGetLastError();
    std::cout << "  returned normally; hipGetLastError() = "
              << hipGetErrorString(err) << std::endl;

    qreal amp = 1 / std::sqrt((qreal) numAmps);
    check("amp[1]", std::real(getQuregAmp(q, 1)), -amp);   // Z negates odd indices

    // [B] one-target dense -> 2^(n-1) threads
    std::cout << "[B] applyHadamard (" << half << " threads)" << std::endl;
    initZeroState(q);
    applyHadamard(q, n - 1);

    qreal invRoot2 = 0.70710678118654752;
    check("amp[0]",    std::real(getQuregAmp(q, 0)),    invRoot2);
    check("amp[half]", std::real(getQuregAmp(q, half)), invRoot2);

    std::cout << (failures ? "RESULT: FAIL" : "RESULT: PASS") << std::endl;

    destroyQureg(q);
    finalizeQuESTEnv();
    return failures ? EXIT_FAILURE : EXIT_SUCCESS;
}

Built with -DUSER_SOURCE_NAMES=repro.cpp -DUSER_OUTPUT_EXE_NAME=repro, plus -DCMAKE_EXE_LINKER_FLAGS="-L$ROCM_PATH/lib -lamdhip64" for the two extern "C" declarations.

Observed

$ ./repro 31
[A] applyPauliZ (2147483648 threads)
  returned normally; hipGetLastError() = no error
  pass  amp[1] = -2.15792e-05  (expected -2.15792e-05)
[B] applyHadamard (1073741824 threads)
  pass  amp[0] = 0.707107  (expected 0.707107)
  pass  amp[half] = 0.707107  (expected 0.707107)
RESULT: PASS

$ ./repro 32
[A] applyPauliZ (4294967296 threads)
  returned normally; hipGetLastError() = invalid configuration argument
  FAIL  amp[1] = 1.52588e-05  (expected -1.52588e-05)
[B] applyHadamard (2147483648 threads)
  pass  amp[0] = 0.707107  (expected 0.707107)
  pass  amp[half] = 0.707107  (expected 0.707107)
RESULT: FAIL

$ ./repro 33
[A] applyPauliZ (8589934592 threads)
  returned normally; hipGetLastError() = invalid configuration argument
  FAIL  amp[1] = 1.07896e-05  (expected -1.07896e-05)
[B] applyHadamard (4294967296 threads)
  FAIL  amp[0] = 1  (expected 0.707107)
  FAIL  amp[half] = 0  (expected 0.707107)
RESULT: FAIL

At 32 qubits applyPauliZ is a complete no-op yet returns normally. At 33 qubits applyHadamard also becomes a no-op: amp[0] = 1, amp[half] = 0 means the register is untouched.

Cause

ROCm limits a grid to 2^32 - 1 total work-items; rocminfo reports Grid Max Size: 4294967295(0xffffffff). QuEST launches one thread per iteration:
gpu_statevector_anyCtrlAnyTargZOrPhaseGadget_sub: numThreads = qureg.numAmpsPerNode / powerOf2(ctrls.size()), so 2^n uncontrolled
gpu_statevec_anyCtrlOneTargDenseMatr_subA: numThreads = qureg.numAmpsPerNode / powerOf2(ctrls.size() + 1), so 2^(n-1) uncontrolled
At 32 qubits the first is exactly 2^32 and the launch is rejected. The failure is silent because kernel launches are not error-checked. CUDA_CHECK is applied consistently to API calls, but no <<<...>>> site is followed by a cudaGetLastError() check.
Thrust-based operations are unaffected: initPlusState and initZeroState are correct at 2^33 amplitudes, since Thrust chunks large ranges internally. Only the hand-written kernels fail.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions