From 52a3b4bd2368d3915cda6c1efa343f3c86fc8ed8 Mon Sep 17 00:00:00 2001 From: Tyson Jones Date: Sat, 14 Jun 2025 19:24:14 +0200 Subject: [PATCH 1/8] changed PERMIT_NODES_TO_SHARE_GPU from macro to env-var This allows it to be changed post-compilation/installation, pre-execution, as per the machination in #645 Additionally inserted whitespaces into cmake error message about MacOS multithreading to make the advised commands clearer --- .github/workflows/test_paid.yml | 5 +++- CMakeLists.txt | 19 +------------ quest/include/environment.h | 3 ++ quest/include/modes.h | 43 ++++++++++++++++++++++------ quest/src/api/environment.cpp | 37 ++++++++++++++---------- quest/src/comm/comm_routines.cpp | 6 ++++ quest/src/core/parser.cpp | 28 +++++++++++++++++++ quest/src/core/parser.hpp | 8 ++++++ quest/src/core/validation.cpp | 48 +++++++++++++++++++++++++++----- quest/src/core/validation.hpp | 9 +++++- tests/main.cpp | 15 +++++----- tests/unit/environment.cpp | 16 ++++++++--- utils/docs/Doxyfile | 1 + 13 files changed, 177 insertions(+), 61 deletions(-) diff --git a/.github/workflows/test_paid.yml b/.github/workflows/test_paid.yml index 878336f7f..c8fe34c03 100644 --- a/.github/workflows/test_paid.yml +++ b/.github/workflows/test_paid.yml @@ -257,12 +257,15 @@ jobs: -DCMAKE_CUDA_ARCHITECTURES=${{ env.cuda_arch }} -DTEST_ALL_DEPLOYMENTS=${{ env.test_all_deploys }} -DTEST_NUM_MIXED_DEPLOYMENT_REPETITIONS=${{ env.test_repetitions }} - -DPERMIT_NODES_TO_SHARE_GPU=${{ env.mpi_share_gpu }} -DCMAKE_CXX_FLAGS=${{ matrix.mpi == 'ON' && matrix.cuda == 'ON' && '-fno-lto' || '' }} - name: Compile run: cmake --build ${{ env.build_dir }} --parallel + # permit use of single GPU by multiple MPI processes (detriments performance) + - name: Set env-var to permit GPU sharing + run: echo "PERMIT_NODES_TO_SHARE_GPU=${{ env.mpi_share_gpu }}" >> $GITHUB_ENV + # cannot use ctests when distributed, grr! - name: Run GPU + distributed v4 mixed tests (4 nodes sharing 1 GPU) run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 1080c55fc..8418b5ba5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -231,15 +231,6 @@ if ((ENABLE_CUDA OR ENABLE_HIP) AND FLOAT_PRECISION STREQUAL 4) message(FATAL_ERROR "Quad precision is not supported on GPU. Please disable GPU acceleration or lower precision.") endif() -option( - PERMIT_NODES_TO_SHARE_GPU - "Whether to permit multiple distributed nodes to share a single GPU at the detriment of performance. Turned OFF by default." - OFF -) -if (ENABLE_DISTRIBUTION AND (ENABLE_CUDA OR ENABLE_HIP)) - message(STATUS "Permitting nodes to share GPUs is turned ${PERMIT_NODES_TO_SHARE_GPU}. Set PERMIT_NODES_TO_SHARE_GPU to modify.") -endif() - # Deprecated API option( ENABLE_DEPRECATED_API @@ -318,7 +309,7 @@ if (ENABLE_MULTITHREADING) if (NOT OpenMP_FOUND) set(ErrorMsg "Could not find OpenMP, necessary for enabling multithreading.") if (APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") - string(APPEND ErrorMsg " Try first calling `brew install libomp` then `export OpenMP_ROOT=$(brew --prefix)/opt/libomp`") + string(APPEND ErrorMsg " Try first calling \n\tbrew install libomp\nthen\n\texport OpenMP_ROOT=$(brew --prefix)/opt/libomp") endif() message(FATAL_ERROR ${ErrorMsg}) endif() @@ -434,14 +425,6 @@ else() endif() -if (ENABLE_DISTRIBUTION AND (ENABLE_CUDA OR ENABLE_HIP)) - target_compile_definitions( - QuEST PRIVATE - PERMIT_NODES_TO_SHARE_GPU=$,1,0> - ) -endif() - - # add math library if (NOT MSVC) target_link_libraries(QuEST PRIVATE ${MATH_LIBRARY}) diff --git a/quest/include/environment.h b/quest/include/environment.h index a71454f0e..04f24bfe2 100644 --- a/quest/include/environment.h +++ b/quest/include/environment.h @@ -40,6 +40,9 @@ typedef struct { // deployment modes which cannot be directly changed after compilation int isCuQuantumEnabled; + // deployment configurations which can be changed via environment variables + int isGpuSharingEnabled; + // distributed configuration int rank; int numNodes; diff --git a/quest/include/modes.h b/quest/include/modes.h index b90797acd..3d8ccc622 100644 --- a/quest/include/modes.h +++ b/quest/include/modes.h @@ -75,10 +75,6 @@ // define optional-macro defaults (mostly to list them) -#ifndef PERMIT_NODES_TO_SHARE_GPU -#define PERMIT_NODES_TO_SHARE_GPU 0 -#endif - #ifndef INCLUDE_DEPRECATED_FUNCTIONS #define INCLUDE_DEPRECATED_FUNCTIONS 0 #endif @@ -93,11 +89,6 @@ #if 0 - /// @notyetdoced - /// @macrodoc - const int PERMIT_NODES_TO_SHARE_GPU = 0; - - /// @notyetdoced /// @macrodoc const int INCLUDE_DEPRECATED_FUNCTIONS = 0; @@ -112,6 +103,40 @@ +// document environment variables + +// spoof env-vars as consts to doc (hackily and hopefully temporarily) +#if 0 + + + /** @envvardoc + * + * Specifies whether to permit multiple MPI processes to deploy to the same GPU. + * + * @attention + * This environment variable has no effect when either (or both) of distribution or + * GPU-acceleration are disabled. + * + * In multi-GPU execution, which combines distribution with GPU-acceleration, it is + * prudent to assign each GPU to at most one MPI process in order to avoid superfluous + * slowdown. Hence by default, initQuESTEnv() will forbid assigning multiple MPI processes + * to the same GPU. This environment variable can be set to `1` to disable this validation, + * permitting sharing of a single GPU, as is often useful for debugging or unit testing + * (for example, testing multi-GPU execution when only a single GPU is available). + * + * @par Values + * - forbid sharing: @p 0, @p '0', @p '', @p , (unspecified) + * - permit sharing: @p 1, @p '1' + * + * @author Tyson Jones + */ + const int PERMIT_NODES_TO_SHARE_GPU = 0; + + +#endif + + + // user flags for choosing automatic deployment; only accessible by C++ // backend and C++ users; C users must hardcode -1 diff --git a/quest/src/api/environment.cpp b/quest/src/api/environment.cpp index 63b6f41ef..959e93693 100644 --- a/quest/src/api/environment.cpp +++ b/quest/src/api/environment.cpp @@ -11,6 +11,7 @@ #include "quest/src/core/errors.hpp" #include "quest/src/core/memory.hpp" +#include "quest/src/core/parser.hpp" #include "quest/src/core/printer.hpp" #include "quest/src/core/autodeployer.hpp" #include "quest/src/core/validation.hpp" @@ -102,12 +103,18 @@ void validateAndInitCustomQuESTEnv(int useDistrib, int useGpuAccel, int useMulti if (useGpuAccel) gpu_bindLocalGPUsToNodes(); - // each MPI process must use a unique GPU. This is critical when - // initializing cuQuantum, so we don't re-init cuStateVec on any - // paticular GPU (causing runtime error), but still ensures we - // keep good performance in our custom backend GPU code; there is - // no reason to use multi-nodes-per-GPU except for dev/debugging. - if (useGpuAccel && useDistrib && ! PERMIT_NODES_TO_SHARE_GPU) + // consult environment variable to decide whether to allow GPU sharing + // (default 'no'=0) which informs whether below validation is triggered + bool permitGpuSharing = parser_validateAndParseOptionalBoolEnvVar( + "PERMIT_NODES_TO_SHARE_GPU", false, caller); + + // each MPI process should ordinarily use a unique GPU. This is + // critical when initializing cuQuantum so that we don't re-init + // cuStateVec on any paticular GPU (which can apparently cause a + // so-far-unwitnessed runtime error), but is otherwise essential + // for good performance. GPU sharing is useful for unit testing + // however permitting a single GPU to test CUDA+MPI deployment + if (useGpuAccel && useDistrib && ! permitGpuSharing) validate_newEnvNodesEachHaveUniqueGpu(caller); /// @todo @@ -132,10 +139,11 @@ void validateAndInitCustomQuESTEnv(int useDistrib, int useGpuAccel, int useMulti error_allocOfQuESTEnvFailed(); // bind deployment info to global instance - globalEnvPtr->isMultithreaded = useMultithread; - globalEnvPtr->isGpuAccelerated = useGpuAccel; - globalEnvPtr->isDistributed = useDistrib; - globalEnvPtr->isCuQuantumEnabled = useCuQuantum; + globalEnvPtr->isMultithreaded = useMultithread; + globalEnvPtr->isGpuAccelerated = useGpuAccel; + globalEnvPtr->isDistributed = useDistrib; + globalEnvPtr->isCuQuantumEnabled = useCuQuantum; + globalEnvPtr->isGpuSharingEnabled = permitGpuSharing; // bind distributed info globalEnvPtr->rank = (useDistrib)? comm_getRank() : 0; @@ -188,10 +196,11 @@ void printDeploymentInfo() { print_table( "deployment", { - {"isMpiEnabled", globalEnvPtr->isDistributed}, - {"isGpuEnabled", globalEnvPtr->isGpuAccelerated}, - {"isOmpEnabled", globalEnvPtr->isMultithreaded}, - {"isCuQuantumEnabled", globalEnvPtr->isCuQuantumEnabled}, + {"isMpiEnabled", globalEnvPtr->isDistributed}, + {"isGpuEnabled", globalEnvPtr->isGpuAccelerated}, + {"isOmpEnabled", globalEnvPtr->isMultithreaded}, + {"isCuQuantumEnabled", globalEnvPtr->isCuQuantumEnabled}, + {"isGpuSharingEnabled", globalEnvPtr->isGpuSharingEnabled}, }); } diff --git a/quest/src/comm/comm_routines.cpp b/quest/src/comm/comm_routines.cpp index 3c03d23f6..6e161db18 100644 --- a/quest/src/comm/comm_routines.cpp +++ b/quest/src/comm/comm_routines.cpp @@ -76,6 +76,12 @@ using std::vector; * * - look into UCX CUDA multi-rail: * https://docs.nvidia.com/networking/display/hpcxv215/unified+communication+-+x+framework+library#src-119764120_UnifiedCommunicationXFrameworkLibrary-Multi-RailMulti-Rail + * + * - by default, we validate to prevent sharing a GPU between multiple MPI processes since it is + * easy to do unintentionally yet is rarely necessary (outside of unit testing) and can severely + * degrade performance. If we motivated a strong non-testing use-case for this however, we could + * improve performance through use of CUDA's Multi-Process Service (MPS) which will prevent + * serialisation of memcpy to distinct memory partitions and improve kernel scheduling. */ diff --git a/quest/src/core/parser.cpp b/quest/src/core/parser.cpp index 31dad4e5f..c83afcba0 100644 --- a/quest/src/core/parser.cpp +++ b/quest/src/core/parser.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -443,3 +444,30 @@ string parser_loadFile(string fn) { buffer << file.rdbuf(); return buffer.str(); } + + + +/* + * ENVIRONMENT VARIABLES + */ + + +bool parser_isStrEmpty(const char* str) { + + // str can be unallocated or empty, but not e.g. whitespace + return (str == nullptr) || (str[0] == '\0'); +} + + +bool parser_validateAndParseOptionalBoolEnvVar(string varName, bool defaultVal, const char* caller) { + + const char* varStr = std::getenv(varName.c_str()); + + // permit specifying no or empty environment variable (triggering default) + if (parser_isStrEmpty(varStr)) + return defaultVal; + + // otherwise it must be precisely 0 or 1 without whitespace + validate_envVarIsBoolean(varName, varStr, caller); + return (varStr[0] == '0')? 0 : 1; +} diff --git a/quest/src/core/parser.hpp b/quest/src/core/parser.hpp index 3e9d18c11..4aa1e8726 100644 --- a/quest/src/core/parser.hpp +++ b/quest/src/core/parser.hpp @@ -42,5 +42,13 @@ bool parser_canReadFile(string fn); string parser_loadFile(string fn); +/* + * ENVIRONMENT VARIABLES + */ + +bool parser_isStrEmpty(const char* str); + +bool parser_validateAndParseOptionalBoolEnvVar(string varName, bool defaultVal, const char* caller); + #endif // PARSER_HPP \ No newline at end of file diff --git a/quest/src/core/validation.cpp b/quest/src/core/validation.cpp index 4b3406975..97d399f11 100644 --- a/quest/src/core/validation.cpp +++ b/quest/src/core/validation.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -1066,6 +1067,19 @@ namespace report { string TEMP_ALLOC_FAILED = "A temporary allocation of ${NUM_ELEMS} elements (each of ${NUM_BYTES_PER_ELEM} bytes) failed, possibly because of insufficient memory."; + + /* + * ENVIRONMENT VARIABLES + */ + + string COMPULSORY_ENV_VAR_WAS_NOT_SPECIFIED_OR_EMPTY = + "A compulsory (but alas here unspecified) environment variable was not set, or was set to an empty string."; + + string INVALID_BOOLEAN_ENVIRONMENT_VARIABLE = + "A boolean environment variable (alas here unspecified) was given a value other than '0' or '1'."; + + string INVALID_PERMIT_NODES_TO_SHARE_GPU_ENV_VAR = + "The optional, boolean PERMIT_NODES_TO_SHARE_GPU environment variable was specified to a value other than '', '0' or '1'."; } @@ -1364,13 +1378,8 @@ void validate_newEnvDistributedBetweenPower2Nodes(const char* caller) { void validate_newEnvNodesEachHaveUniqueGpu(const char* caller) { - // this validation can be disabled for debugging/dev purposes - // (caller should explicitly check this preprocessor too for clarity) - if (PERMIT_NODES_TO_SHARE_GPU) - return; - - bool uniqueGpus = ! gpu_areAnyNodesBoundToSameGpu(); - assertAllNodesAgreeThat(uniqueGpus, report::MULTIPLE_NODES_BOUND_TO_SAME_GPU, caller); + bool sharedGpus = gpu_areAnyNodesBoundToSameGpu(); + assertAllNodesAgreeThat(!sharedGpus, report::MULTIPLE_NODES_BOUND_TO_SAME_GPU, caller); } void validate_gpuIsCuQuantumCompatible(const char* caller) { @@ -4165,3 +4174,28 @@ void validate_tempAllocSucceeded(bool succeeded, qindex numElems, qindex numByte assertThat(succeeded, report::TEMP_ALLOC_FAILED, vars, caller); } + + + +/* + * ENVIRONMENT VARIABLES + */ + +void validate_envVarIsBoolean(string varName, const char* varStr, const char* caller) { + + // empty non-compulsory environment vars never reach this validation function + assertThat(!parser_isStrEmpty(varStr), report::COMPULSORY_ENV_VAR_WAS_NOT_SPECIFIED_OR_EMPTY, caller); + + // value must be a single 0 or 1 character (below expr works even when str has no terminal) + bool isValid = (varStr[0] == '0' || varStr[0] == '1') && (varStr[1] == '\0'); + + /// @todo include 'varName' in printed vars once tokenSubs can support strings + // hackily ensure "PERMIT_NODES_TO_SHARE_GPU" is featured in the error message as + // the only currently supported environment variable and is important to specify + string errMsg = (varName == "PERMIT_NODES_TO_SHARE_GPU")? + report::INVALID_PERMIT_NODES_TO_SHARE_GPU_ENV_VAR : + report::INVALID_BOOLEAN_ENVIRONMENT_VARIABLE; + + /// @todo include 'varStr' in printed vars once tokenSubs can support strings + assertThat(isValid, errMsg, caller); +} diff --git a/quest/src/core/validation.hpp b/quest/src/core/validation.hpp index 0bf48b409..cf8b62535 100644 --- a/quest/src/core/validation.hpp +++ b/quest/src/core/validation.hpp @@ -488,7 +488,6 @@ void validate_densMatrExpecDiagMatrValueIsReal(qcomp value, qcomp exponent, cons * PARTIAL TRACE */ - void validate_quregCanBeReduced(Qureg qureg, int numTraceQubits, const char* caller); void validate_quregCanBeSetToReducedDensMatr(Qureg out, Qureg in, int numTraceQubits, const char* caller); @@ -511,4 +510,12 @@ void validate_tempAllocSucceeded(bool succeeded, qindex numElems, qindex numByte +/* + * ENVIRONMENT VARIABLES + */ + +void validate_envVarIsBoolean(std::string varName, const char* varStr, const char* caller); + + + #endif // VALIDATION_HPP \ No newline at end of file diff --git a/tests/main.cpp b/tests/main.cpp index 03294857a..29647753e 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -88,13 +88,14 @@ class startListener : public Catch::EventListenerBase { QuESTEnv env = getQuESTEnv(); std::cout << std::endl; std::cout << "QuEST execution environment:" << std::endl; - std::cout << " precision: " << FLOAT_PRECISION << std::endl; - std::cout << " multithreaded: " << env.isMultithreaded << std::endl; - std::cout << " distributed: " << env.isDistributed << std::endl; - std::cout << " GPU-accelerated: " << env.isGpuAccelerated << std::endl; - std::cout << " cuQuantum: " << env.isCuQuantumEnabled << std::endl; - std::cout << " num nodes: " << env.numNodes << std::endl; - std::cout << " num qubits: " << getNumCachedQubits() << std::endl; + std::cout << " precision: " << FLOAT_PRECISION << std::endl; + std::cout << " multithreaded: " << env.isMultithreaded << std::endl; + std::cout << " distributed: " << env.isDistributed << std::endl; + std::cout << " GPU-accelerated: " << env.isGpuAccelerated << std::endl; + std::cout << " GPU-sharing ok: " << env.isGpuSharingEnabled << std::endl; + std::cout << " cuQuantum: " << env.isCuQuantumEnabled << std::endl; + std::cout << " num nodes: " << env.numNodes << std::endl; + std::cout << " num qubits: " << getNumCachedQubits() << std::endl; std::cout << " num qubit perms: " << TEST_MAX_NUM_QUBIT_PERMUTATIONS << std::endl; std::cout << std::endl; diff --git a/tests/unit/environment.cpp b/tests/unit/environment.cpp index db9a1516c..6d4efb80d 100644 --- a/tests/unit/environment.cpp +++ b/tests/unit/environment.cpp @@ -54,6 +54,13 @@ TEST_CASE( "initQuESTEnv", TEST_CATEGORY ) { SECTION( LABEL_VALIDATION ) { REQUIRE_THROWS_WITH( initQuESTEnv(), ContainsSubstring( "already been initialised") ); + + // cannot automatically check other validations, such as: + // - has env been previously initialised then finalised? + // - is env distributed over power-of-2 nodes? + // - are environment-variables valid? + // - is max 1 MPI process bound to each GPU? + // - is GPU compatible with cuQuantum (if enabled)? } } @@ -133,10 +140,11 @@ TEST_CASE( "getQuESTEnv", TEST_CATEGORY ) { QuESTEnv env = getQuESTEnv(); - REQUIRE( (env.isMultithreaded == 0 || env.isMultithreaded == 1) ); - REQUIRE( (env.isGpuAccelerated == 0 || env.isGpuAccelerated == 1) ); - REQUIRE( (env.isDistributed == 0 || env.isDistributed == 1) ); - REQUIRE( (env.isCuQuantumEnabled == 0 || env.isCuQuantumEnabled == 1) ); + REQUIRE( (env.isMultithreaded == 0 || env.isMultithreaded == 1) ); + REQUIRE( (env.isGpuAccelerated == 0 || env.isGpuAccelerated == 1) ); + REQUIRE( (env.isDistributed == 0 || env.isDistributed == 1) ); + REQUIRE( (env.isCuQuantumEnabled == 0 || env.isCuQuantumEnabled == 1) ); + REQUIRE( (env.isGpuSharingEnabled == 0 || env.isGpuSharingEnabled == 1) ); REQUIRE( env.rank >= 0 ); REQUIRE( env.numNodes >= 0 ); diff --git a/utils/docs/Doxyfile b/utils/docs/Doxyfile index 4eb40b524..f113eaba5 100644 --- a/utils/docs/Doxyfile +++ b/utils/docs/Doxyfile @@ -301,6 +301,7 @@ ALIASES += "notyetdoced=@note Documentation for this function or struct is under ALIASES += "cpponly=@remark This function is only available in C++." ALIASES += "conly=@remark This function is only available in C." ALIASES += "macrodoc=@note This entity is actually a macro." +ALIASES += "envvardoc=@note This entity is actually an environment variable." ALIASES += "neverdoced=@warning This entity is a macro, undocumented directly due to a Doxygen limitation. If you see this doc rendered, contact the devs!" ALIASES += "myexample=@par Example" ALIASES += "equivalences=@par Equivalences" From fd49f55b149095e93b6ff33abd5918baac248274 Mon Sep 17 00:00:00 2001 From: Tyson Jones Date: Sat, 14 Jun 2025 19:27:57 +0200 Subject: [PATCH 2/8] added cuQuantum warning --- quest/include/modes.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quest/include/modes.h b/quest/include/modes.h index 3d8ccc622..b29dc626a 100644 --- a/quest/include/modes.h +++ b/quest/include/modes.h @@ -124,6 +124,9 @@ * permitting sharing of a single GPU, as is often useful for debugging or unit testing * (for example, testing multi-GPU execution when only a single GPU is available). * + * @warning + * Permitting GPU sharing may cause unintended behaviour when additionally using cuQuantum. + * * @par Values * - forbid sharing: @p 0, @p '0', @p '', @p , (unspecified) * - permit sharing: @p 1, @p '1' From a66f797b69d2ed7cea07ca09b19b9d5487511bdb Mon Sep 17 00:00:00 2001 From: Tyson Jones Date: Sat, 21 Jun 2025 22:32:45 +0200 Subject: [PATCH 3/8] patched esoteric bug in parser which previously made use of stold() to validate that a user-given string contained a number which can be parsed as a qreal. This enabled a bug where users using FLOAT_PRECISION=1 or 2 could specify a number (e.g. in createInlinePauliStrSum) which is well-formed but exceeds the maximum or minimum storable qreal. By using stold() (the long-double version of "string to float"), the validation was too lenient and permitted literals of numbers which can fit into a long-double-qreal but not the current smaller-precision qreal. We now instead use a string-to-float function specific to the precision of qreal (stof(), stod() or stold()) so that the "can store number as qreal" validation always runs correctly. We further refactored number parsing in parser.cpp to "trust" the regex and throw an internal error (rather than a user-blaming validation error) when the string-to-float functions fail when the regex assured otherwise. This --- quest/src/core/parser.cpp | 262 +++++++++++++++++++++++++------------- quest/src/core/parser.hpp | 24 ++-- 2 files changed, 189 insertions(+), 97 deletions(-) diff --git a/quest/src/core/parser.cpp b/quest/src/core/parser.cpp index c83afcba0..758cff64e 100644 --- a/quest/src/core/parser.cpp +++ b/quest/src/core/parser.cpp @@ -10,6 +10,7 @@ * @author Tyson Jones */ +#include "quest/include/precision.h" #include "quest/include/types.h" #include "quest/include/paulis.h" @@ -22,11 +23,9 @@ #include #include #include -#include #include #include -using std::stold; using std::regex; using std::vector; using std::string; @@ -83,9 +82,9 @@ namespace patterns { string num = group(comp) + "|" + group(imag) + "|" + group(real); // no capturing because 'num' pollutes captured groups, and pauli syntax overlaps real integers - string pauli = "[" + parser_RECOGNISED_PAULI_CHARS + "]"; + string pauli = "[" + parser_RECOGNISED_PAULI_CHARS + "]"; string paulis = group(optSpace + pauli + optSpace) + "+"; - string line = "^" + group(num) + space + optSpace + paulis + "$"; + string weightedPaulis = "^" + group(num) + space + optSpace + paulis + "$"; } @@ -96,8 +95,8 @@ namespace regexes { regex imag(patterns::imag); regex comp(patterns::comp); regex num(patterns::num); - regex line(patterns::line); regex paulis(patterns::paulis); + regex weightedPaulis(patterns::weightedPaulis); } @@ -173,6 +172,165 @@ int getNumPaulisInLine(string line) { +/* + * REAL NUMBER PARSING + */ + + +qreal precisionAgnosticStringToFloat(string str) { + + // remove whitespace which stold() et al cannot handle after the sign. + // beware this means that e.g. "1 0" (invalid number) would become "10" + // (valid) so this function cannot be used for duck-typing, though that + // is anyway the case since stold() et al permit "10abc" + removeWhiteSpace(str); + + // below throws exception when the (prefix) of str cannot be/fit into a qreal + if (FLOAT_PRECISION == 1) return static_cast(std::stof (str)); + if (FLOAT_PRECISION == 2) return static_cast(std::stod (str)); + if (FLOAT_PRECISION == 4) return static_cast(std::stold(str)); + + // unreachable + return -1; +} + + +bool parser_isAnySizedReal(string str) { + + // we assume that all strings which match the regex can be parsed by + // precisionAgnosticStringToFloat() above (once whitespace is removed) + // EXCEPT strings which contain a number too large to store in the qreal + // type (as is separately checked below). Note it is insufficient to merely + // duck-type using stold() et al because such functions permit non-numerical + // characters to follow the contained number (grr!) + smatch match; + return regex_match(str, match, regexes::real); +} + + +bool parser_isValidReal(string str) { + + // reject str if it doesn't match regex + if (!parser_isAnySizedReal(str)) + return false; + + // check number is in-range of qreal via duck-typing + try { + precisionAgnosticStringToFloat(str); + } catch (const out_of_range&) { + return false; + + // error if our regex permitted an unparsable string + } catch (const invalid_argument&) { + error_attemptedToParseRealFromInvalidString(); + } + + return true; +} + + +qreal parser_parseReal(string str) { + + try { + return precisionAgnosticStringToFloat(str); + } catch (const invalid_argument&) { + error_attemptedToParseRealFromInvalidString(); + } catch (const out_of_range&) { + error_attemptedToParseOutOfRangeReal(); + } + + // unreachable + return -1; +} + + + +/* + * COMPLEX NUMBER PARSING + */ + + +bool parser_isAnySizedComplex(string str) { + + // we assume that all strings which match the regex can be parsed to + // a qcomp (once whitespace is removed) EXCEPT strings which contain a + // number too large to store in the qcomp type (as is separately checked + // below). Note it is insufficient to merely duck-type each component using + // using stold() et al because such functions permit non-numerical chars to + // follow the contained number (grr!) + smatch match; + + // must match real, imaginary or complex number regex + if (regex_match(str, match, regexes::real)) return true; + if (regex_match(str, match, regexes::imag)) return true; + if (regex_match(str, match, regexes::comp)) return true; + + return false; +} + + +bool parser_isValidComplex(string str) { + + // reject str if it doesn't match complex regex + if (!parser_isAnySizedComplex(str)) + return false; + + // we've so far gauranteed str has a valid form, but we must now check + // each included complex component (which we enumerate) is in range of a qreal + sregex_iterator it(str.begin(), str.end(), regexes::real); + sregex_iterator end; + + // valid coeffs contain 1 or 2 reals, never 0, which regex should have caught + if (it == end) + error_attemptedToParseComplexFromInvalidString(); + + // for each of the 1 or 2 components... + for (; it != end; it++) { + + // check component is in-range of qreal via duck-typing + try { + precisionAgnosticStringToFloat(it->str(0)); + } catch (const out_of_range&) { + return false; + + // error if our regex permitted an unparsable component + } catch (const invalid_argument&) { + error_attemptedToParseComplexFromInvalidString(); + } + } + + // report that each/all detected components of str can form a valid qcomp + return true; +} + + +qcomp parser_parseComplex(string str) { + + if (!parser_isValidComplex(str)) + error_attemptedToParseComplexFromInvalidString(); + + // we are gauranteed to fully match real, imag or comp after prior validation + smatch match; + + // extract and parse components and their signs (excluding imaginary symbol) + if (regex_match(str, match, regexes::real)) + return qcomp(parser_parseReal(match.str(1)), 0); + + if (regex_match(str, match, regexes::imag)) + return qcomp(0, parser_parseReal(match.str(1))); + + if (regex_match(str, match, regexes::comp)) + return qcomp( + parser_parseReal(match.str(1)), + parser_parseReal(match.str(2))); + + // should be unreachable + error_attemptedToParseComplexFromInvalidString(); + return qcomp(0,0); +} + + + /* * VALIDATION * @@ -189,15 +347,14 @@ bool isInterpretablePauliStrSumLine(string line) { // notation) followed by 1 or more space characters, then one or // more pauli codes/chars. It does NOT determine whether the coeff // can actually be instantiated as a qcomp - return regex_match(line, regexes::line); + return regex_match(line, regexes::weightedPaulis); } bool isCoeffValidInPauliStrSumLine(string line) { // it is gauranteed that line is interpretable and contains a regex-matching - // coefficient, but we must additionally verify it is within range of stold, - // and isn't unexpectedly incompatible with stold in a way uncaptured by regex. + // coefficient, but we must additionally verify it is within range of qreal. // So we duck type each of the 1 or 2 matches with the real regex (i.e. one or // both of the real and imaginary components of a complex coeff). @@ -216,17 +373,17 @@ bool isCoeffValidInPauliStrSumLine(string line) { // enumerate all matches of 'real' regex in line for (; it != end; it++) { - // removed whitespace (stold cannot handle space between sign and number) + // remove whitespace (stold cannot handle space between sign and number) string match = it->str(0); removeWhiteSpace(match); - // return false if stold cannot parse the real as a long double + // return false if number cannot become a qreal try { - stold(match); - } catch (const invalid_argument&) { - return false; + precisionAgnosticStringToFloat(match); } catch (const out_of_range&) { return false; + } catch (const invalid_argument&) { // should be impossible (indicates bad regex) + return false; } } @@ -300,52 +457,6 @@ int parser_getPauliIntFromChar(char ch) { */ -qreal parseReal(string real) { - - // attempt to parse at max precision (long double) then cast down if necessary - try { - return static_cast(stold(real)); - - // should be impossible if regex and validation works correctly - } catch (const invalid_argument&) { - error_attemptedToParseRealFromInvalidString(); - - // should be prior caught by validation - } catch (const out_of_range&) { - error_attemptedToParseOutOfRangeReal(); - } - - // unreachable - return -1; -} - - -qcomp parseCoeff(string coeff) { - - // remove all superfluous spaces in coeff so stold is happy (it cannot tolerate spaces after +-) - removeWhiteSpace(coeff); - - // we are gauranteed to fully match real, imag or comp after prior validation - smatch match; - - // extract and parse components and their signs (excluding imaginary symbol) - if (regex_match(coeff, match, regexes::real)) - return qcomp(parseReal(match.str(1)), 0); - - if (regex_match(coeff, match, regexes::imag)) - return qcomp(0, parseReal(match.str(1))); - - if (regex_match(coeff, match, regexes::comp)) - return qcomp( - parseReal(match.str(1)), - parseReal(match.str(2))); - - // should be unreachable - error_attemptedToParseComplexFromInvalidString(); - return qcomp(0,0); -} - - PauliStr parsePaulis(string paulis, bool rightIsLeastSignificant) { // remove whitespace to make string compatible with getPauliStr() @@ -364,14 +475,14 @@ PauliStr parsePaulis(string paulis, bool rightIsLeastSignificant) { } -void parseLine(string line, qcomp &coeff, PauliStr &pauli, bool rightIsLeastSignificant) { +void parseWeightedPaulis(string line, qcomp &coeff, PauliStr &pauli, bool rightIsLeastSignificant) { // separate line into substrings string coeffStr, pauliStr; separateStringIntoCoeffAndPaulis(line, coeffStr, pauliStr); // parse each, overwriting calller primitives - coeff = parseCoeff(coeffStr); + coeff = parser_parseComplex(coeffStr); pauli = parsePaulis(pauliStr, rightIsLeastSignificant); } @@ -403,7 +514,7 @@ PauliStrSum parser_validateAndParsePauliStrSum(string lines, bool rightIsLeastSi qcomp coeff; PauliStr string; - parseLine(line, coeff, string, rightIsLeastSignificant); // validates + parseWeightedPaulis(line, coeff, string, rightIsLeastSignificant); // validates coeffs.push_back(coeff); strings.push_back(string); @@ -444,30 +555,3 @@ string parser_loadFile(string fn) { buffer << file.rdbuf(); return buffer.str(); } - - - -/* - * ENVIRONMENT VARIABLES - */ - - -bool parser_isStrEmpty(const char* str) { - - // str can be unallocated or empty, but not e.g. whitespace - return (str == nullptr) || (str[0] == '\0'); -} - - -bool parser_validateAndParseOptionalBoolEnvVar(string varName, bool defaultVal, const char* caller) { - - const char* varStr = std::getenv(varName.c_str()); - - // permit specifying no or empty environment variable (triggering default) - if (parser_isStrEmpty(varStr)) - return defaultVal; - - // otherwise it must be precisely 0 or 1 without whitespace - validate_envVarIsBoolean(varName, varStr, caller); - return (varStr[0] == '0')? 0 : 1; -} diff --git a/quest/src/core/parser.hpp b/quest/src/core/parser.hpp index 4aa1e8726..4a9df2d02 100644 --- a/quest/src/core/parser.hpp +++ b/quest/src/core/parser.hpp @@ -7,6 +7,7 @@ #ifndef PARSER_HPP #define PARSER_HPP +#include "quest/include/types.h" #include "quest/include/paulis.h" #include @@ -15,6 +16,21 @@ using std::string; +/* + * PARSING NUMBERS + */ + +bool parser_isAnySizedReal(string str); +bool parser_isAnySizedComplex(string str); + +bool parser_isValidReal(string str); +bool parser_isValidComplex(string str); + +qreal parser_parseReal(string str); +qcomp parser_parseComplex(string str); + + + /* * PARSING INDIVIDUAL PAULIS */ @@ -42,13 +58,5 @@ bool parser_canReadFile(string fn); string parser_loadFile(string fn); -/* - * ENVIRONMENT VARIABLES - */ - -bool parser_isStrEmpty(const char* str); - -bool parser_validateAndParseOptionalBoolEnvVar(string varName, bool defaultVal, const char* caller); - #endif // PARSER_HPP \ No newline at end of file From 8ec0172e9ec66cb11417d0fadc0483f01d71f6fd Mon Sep 17 00:00:00 2001 From: Tyson Jones Date: Sat, 21 Jun 2025 22:33:32 +0200 Subject: [PATCH 4/8] centralised handling of environment variables and changed DEFAULT_VALIDATION_EPSILON from a macro to an env-var --- quest/include/modes.h | 30 ++++++ quest/include/precision.h | 31 ++----- quest/src/api/environment.cpp | 9 +- quest/src/core/CMakeLists.txt | 1 + quest/src/core/envvars.cpp | 158 ++++++++++++++++++++++++++++++++ quest/src/core/envvars.hpp | 31 +++++++ quest/src/core/errors.cpp | 16 ++++ quest/src/core/errors.hpp | 10 ++ quest/src/core/validation.cpp | 47 +++++----- quest/src/core/validation.hpp | 4 +- quest/src/gpu/gpu_cuquantum.cuh | 7 +- 11 files changed, 293 insertions(+), 51 deletions(-) create mode 100644 quest/src/core/envvars.cpp create mode 100644 quest/src/core/envvars.hpp diff --git a/quest/include/modes.h b/quest/include/modes.h index b29dc626a..f7d53c3c8 100644 --- a/quest/include/modes.h +++ b/quest/include/modes.h @@ -136,6 +136,36 @@ const int PERMIT_NODES_TO_SHARE_GPU = 0; + /** @envvardoc + * + * Specifies the default validation epsilon. + * + * Specifying `DEFAULT_VALIDATION_EPSILON` to a positive, real number overrides the + * precision-specific default (`1E-5`, `1E-12`, `1E-13` for single, double and quadruple + * precision respectively). The specified epsilon is used by QuEST for numerical validation + * unless overriden at runtime via setValidationEpsilon(), in which case it can be + * restored to that specified by this environment variable using setValidationEpsilonToDefault(). + * + * @par Values + * - setting @p DEFAULT_VALIDATION_EPSILON=0 disables numerical validation, as if the value + * were instead infinity. + * - setting @p DEFAULT_VALIDATION_EPSILON='' is equivalent to _not_ specifying the variable, + * adopting instead the precision-specific default above. + * - setting @p DEFAULT_VALIDATION_EPSILON=x where `x` is a positive, valid `qreal` in any + * format accepted by `C` or `C++` (e.g. `0.01`, `1E-2`, `+1e-2`) will use `x` as the + * default validation epsilon. + * + * @constraints + * The function initQuESTEnv() will throw a validation error if: + * - The specified epsilon must be `0` or positive. + * - The specified epsilon must not exceed that maximum or minimum value which can be stored + * in a `qreal`, which is specific to its precision. + * + * @author Tyson Jones + */ + const qreal DEFAULT_VALIDATION_EPSILON = 0; + + #endif diff --git a/quest/include/precision.h b/quest/include/precision.h index cfd150855..f7a18e416 100644 --- a/quest/include/precision.h +++ b/quest/include/precision.h @@ -121,34 +121,19 @@ /* - * RE-CONFIGURABLE DEFAULT VALIDATION PRECISION + * DEFAULT VALIDATION PRECISION * - * which is compile-time overridable by pre-defining DEFAULT_VALIDATION_EPSILON (e.g. - * in user code before importing QuEST, or passed as a preprocessor constant by the - * compiler using argument -D), and runtime overridable using setValidationEpsilon() + * which is pre-run-time overridable by specifying the corresponding environment variable. */ -#ifndef DEFAULT_VALIDATION_EPSILON - - #if FLOAT_PRECISION == 1 - #define DEFAULT_VALIDATION_EPSILON 1E-5 - - #elif FLOAT_PRECISION == 2 - #define DEFAULT_VALIDATION_EPSILON 1E-12 - - #elif FLOAT_PRECISION == 4 - #define DEFAULT_VALIDATION_EPSILON 1E-13 - - #endif - -#endif +#if FLOAT_PRECISION == 1 + #define UNSPECIFIED_DEFAULT_VALIDATION_EPSILON 1E-5 -// spoofing above macros as typedefs and consts to doc -#if 0 +#elif FLOAT_PRECISION == 2 + #define UNSPECIFIED_DEFAULT_VALIDATION_EPSILON 1E-12 - /// @notyetdoced - /// @macrodoc - const qreal DEFAULT_VALIDATION_EPSILON = 1E-12; +#elif FLOAT_PRECISION == 4 + #define UNSPECIFIED_DEFAULT_VALIDATION_EPSILON 1E-13 #endif diff --git a/quest/src/api/environment.cpp b/quest/src/api/environment.cpp index 959e93693..6eef515c4 100644 --- a/quest/src/api/environment.cpp +++ b/quest/src/api/environment.cpp @@ -13,6 +13,7 @@ #include "quest/src/core/memory.hpp" #include "quest/src/core/parser.hpp" #include "quest/src/core/printer.hpp" +#include "quest/src/core/envvars.hpp" #include "quest/src/core/autodeployer.hpp" #include "quest/src/core/validation.hpp" #include "quest/src/core/randomiser.hpp" @@ -76,6 +77,9 @@ void validateAndInitCustomQuESTEnv(int useDistrib, int useGpuAccel, int useMulti // this leads to undefined behaviour in distributed mode, as per the MPI validate_envNeverInit(globalEnvPtr != nullptr, hasEnvBeenFinalized, caller); + envvars_validateAndLoadEnvVars(caller); + validateconfig_setEpsilonToDefault(); + // ensure the chosen deployment is compiled and supported by hardware. // note that these error messages will be printed by every node because // validation occurs before comm_init() below, so all processes spawned @@ -104,9 +108,8 @@ void validateAndInitCustomQuESTEnv(int useDistrib, int useGpuAccel, int useMulti gpu_bindLocalGPUsToNodes(); // consult environment variable to decide whether to allow GPU sharing - // (default 'no'=0) which informs whether below validation is triggered - bool permitGpuSharing = parser_validateAndParseOptionalBoolEnvVar( - "PERMIT_NODES_TO_SHARE_GPU", false, caller); + // (default = false) which informs whether below validation is triggered + bool permitGpuSharing = envvars_getWhetherGpuSharingIsPermitted(); // each MPI process should ordinarily use a unique GPU. This is // critical when initializing cuQuantum so that we don't re-init diff --git a/quest/src/core/CMakeLists.txt b/quest/src/core/CMakeLists.txt index e498d4569..9d11d16d7 100644 --- a/quest/src/core/CMakeLists.txt +++ b/quest/src/core/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(QuEST PRIVATE accelerator.cpp autodeployer.cpp + envvars.cpp errors.cpp localiser.cpp memory.cpp diff --git a/quest/src/core/envvars.cpp b/quest/src/core/envvars.cpp new file mode 100644 index 000000000..c88647e0e --- /dev/null +++ b/quest/src/core/envvars.cpp @@ -0,0 +1,158 @@ +/** @file + * Functions for loading environment variables, useful for + * configuring QuEST ahead of calling initQuESTEnv(), after + * compilation. + * + * @author Tyson Jones + */ + +#include "quest/include/precision.h" +#include "quest/include/types.h" + +#include "quest/src/core/errors.hpp" +#include "quest/src/core/parser.hpp" +#include "quest/src/core/validation.hpp" + +#include +#include + +using std::string; + + + +/* + * FIXED ENV-VAR NAMES + */ + + +namespace envvar_names { + string PERMIT_NODES_TO_SHARE_GPU = "PERMIT_NODES_TO_SHARE_GPU"; + string DEFAULT_VALIDATION_EPSILON = "DEFAULT_VALIDATION_EPSILON"; +} + + + +/* + * USER-OVERRIDABLE DEFAULT ENV-VAR VALUES + */ + + +namespace envvar_values { + + // by default, do not permit GPU sharing since it sabotages performance + // and should only ever be carefully, deliberately enabled + bool PERMIT_NODES_TO_SHARE_GPU = false; + + // by default, the initial validation epsilon (before being overriden + // by users at runtime) should depend on qreal (i.e. FLOAT_PRECISION) + qreal DEFAULT_VALIDATION_EPSILON = UNSPECIFIED_DEFAULT_VALIDATION_EPSILON; +} + + +// indicates whether envvars_validateAndLoadEnvVars() has been called +bool global_areEnvVarsLoaded = false; + + + +/* + * PRIVATE UTILITIES + */ + + +bool isEnvVarSpecified(string name) { + + // note var="" is considered unspecified, but var=" " is specified + const char* ptr = std::getenv(name.c_str()); + return (ptr != nullptr) && (ptr[0] != '\0'); +} + + +string getSpecifiedEnvVarValue(string name) { + + // assumes isEnvVarSpecified returned true + // (calling getenv() a second time is fine) + return std::string(std::getenv(name.c_str())); +} + + +void assertEnvVarsAreLoaded() { + + if (!global_areEnvVarsLoaded) + error_envVarsNotYetLoaded(); +} + + + +/* + * PRIVATE BESPOKE ENV-VAR LOADERS + * + * which we have opted to not-yet make generic + * (e.g. for each type) since YAGNI + */ + + +void validateAndSetWhetherGpuSharingIsPermitted(const char* caller) { + + // permit unspecified, falling back to default value + string name = envvar_names::PERMIT_NODES_TO_SHARE_GPU; + if (!isEnvVarSpecified(name)) + return; + + // otherwise ensure value == '0' or '1' precisely (no whitespace) + string value = getSpecifiedEnvVarValue(name); + validate_envVarPermitNodesToShareGpu(value, caller); + + // overwrite default env-var value + envvar_values::PERMIT_NODES_TO_SHARE_GPU = (value[0] == '1'); +} + + +void validateAndSetDefaultValidationEpsilon(const char* caller) { + + // permit unspecified, falling back to the hardcoded precision-specific default + string name = envvar_names::DEFAULT_VALIDATION_EPSILON; + if (!isEnvVarSpecified(name)) + return; + + // otherwise, validate user passed a positive real integer (or zero) + string value = getSpecifiedEnvVarValue(name); + validate_envVarDefaultValidationEpsilon(value, caller); + + // overwrite default env-var value + envvar_values::DEFAULT_VALIDATION_EPSILON = parser_parseReal(value); +} + + + +/* + * PUBLIC + */ + + +void envvars_validateAndLoadEnvVars(const char* caller) { + + // error if loaded twice since this indicates spaghetti + if (global_areEnvVarsLoaded) + error_envVarsAlreadyLoaded(); + + // load all env-vars + validateAndSetWhetherGpuSharingIsPermitted(caller); + validateAndSetDefaultValidationEpsilon(caller); + + // ensure no re-loading + global_areEnvVarsLoaded = true; +} + + +bool envvars_getWhetherGpuSharingIsPermitted() { + assertEnvVarsAreLoaded(); + + return envvar_values::PERMIT_NODES_TO_SHARE_GPU; +} + + +qreal envvars_getDefaultValidationEpsilon() { + assertEnvVarsAreLoaded(); + + return envvar_values::DEFAULT_VALIDATION_EPSILON; +} diff --git a/quest/src/core/envvars.hpp b/quest/src/core/envvars.hpp new file mode 100644 index 000000000..13380ffa0 --- /dev/null +++ b/quest/src/core/envvars.hpp @@ -0,0 +1,31 @@ +/** @file + * Functions for loading environment variables, useful for + * configuring QuEST ahead of calling initQuESTEnv(), after + * compilation. + * + * @author Tyson Jones + */ + +#include + + +namespace envvar_names { + extern std::string PERMIT_NODES_TO_SHARE_GPU; + extern std::string DEFAULT_VALIDATION_EPSILON; +} + + +/* + * LOAD VARS + */ + +void envvars_validateAndLoadEnvVars(const char* caller); + + +/* + * GET VAR + */ + +bool envvars_getWhetherGpuSharingIsPermitted(); + +qreal envvars_getDefaultValidationEpsilon(); diff --git a/quest/src/core/errors.cpp b/quest/src/core/errors.cpp index a2c2649ca..2f44127c8 100644 --- a/quest/src/core/errors.cpp +++ b/quest/src/core/errors.cpp @@ -818,3 +818,19 @@ void assert_printerGivenPositiveNumNewlines() { if (printer_getNumTrailingNewlines() < min) raiseInternalError("A printer utility attempted to print one fewer than the user-set number of trailing newlines; but that number was zero! This violates prior validation."); } + + + +/* + * ENVIRONMENT VARIABLE ERRORS + */ + +void error_envVarsNotYetLoaded() { + + raiseInternalError("An environment variable was queried but all environment variables have not yet been loaded."); +} + +void error_envVarsAlreadyLoaded() { + + raiseInternalError("All environment variables were already loaded and validated yet re-loading was attempted."); +} diff --git a/quest/src/core/errors.hpp b/quest/src/core/errors.hpp index 8c39ee756..ce8f7e68c 100644 --- a/quest/src/core/errors.hpp +++ b/quest/src/core/errors.hpp @@ -339,4 +339,14 @@ void assert_printerGivenPositiveNumNewlines(); +/* + * ENVIRONMENT VARIABLE ERRORS + */ + +void error_envVarsNotYetLoaded(); + +void error_envVarsAlreadyLoaded(); + + + #endif // ERRORS_HPP \ No newline at end of file diff --git a/quest/src/core/validation.cpp b/quest/src/core/validation.cpp index 97d399f11..c5d0007a7 100644 --- a/quest/src/core/validation.cpp +++ b/quest/src/core/validation.cpp @@ -23,6 +23,7 @@ #include "quest/src/core/utilities.hpp" #include "quest/src/core/parser.hpp" #include "quest/src/core/printer.hpp" +#include "quest/src/core/envvars.hpp" #include "quest/src/comm/comm_config.hpp" #include "quest/src/comm/comm_routines.hpp" #include "quest/src/cpu/cpu_config.hpp" @@ -1072,14 +1073,17 @@ namespace report { * ENVIRONMENT VARIABLES */ - string COMPULSORY_ENV_VAR_WAS_NOT_SPECIFIED_OR_EMPTY = - "A compulsory (but alas here unspecified) environment variable was not set, or was set to an empty string."; + string INVALID_PERMIT_NODES_TO_SHARE_GPU_ENV_VAR = + "The optional, boolean '" + envvar_names::PERMIT_NODES_TO_SHARE_GPU + "' environment variable was specified to an invalid value. The variable can be unspecified, or set to '', '0' or '1'."; - string INVALID_BOOLEAN_ENVIRONMENT_VARIABLE = - "A boolean environment variable (alas here unspecified) was given a value other than '0' or '1'."; + string DEFAULT_EPSILON_ENV_VAR_NOT_A_REAL = + "The optional '" + envvar_names::DEFAULT_VALIDATION_EPSILON + "' environment variable was not a recognisable real number."; - string INVALID_PERMIT_NODES_TO_SHARE_GPU_ENV_VAR = - "The optional, boolean PERMIT_NODES_TO_SHARE_GPU environment variable was specified to a value other than '', '0' or '1'."; + string DEFAULT_EPSILON_ENV_VAR_EXCEEDS_QREAL_RANGE = + "The optional '" + envvar_names::DEFAULT_VALIDATION_EPSILON + "' environment variable was larger (in magnitude) than the maximum value which can be stored in a qreal."; + + string DEFAULT_EPSILON_ENV_VAR_IS_NEGATIVE = + "The optional '" + envvar_names::DEFAULT_VALIDATION_EPSILON + "' environment variable was negative. The value must be zero or positive."; } @@ -1167,13 +1171,16 @@ qreal REDUCTION_EPSILON_FACTOR = 100; * overwritten (so will stay validate_STRUCT_PROPERTY_UNKNOWN_FLAG) */ -static qreal global_validationEpsilon = DEFAULT_VALIDATION_EPSILON; +// the default epsilon is not known until runtime since the macro +// UNSPECIFIED_DEFAULT_VALIDATION_EPSILON may be overriden by the +// DEFAULT_VALIDATION_EPSILON environment variable +static qreal global_validationEpsilon = -1; // must be overriden void validateconfig_setEpsilon(qreal eps) { global_validationEpsilon = eps; } void validateconfig_setEpsilonToDefault() { - global_validationEpsilon = DEFAULT_VALIDATION_EPSILON; + global_validationEpsilon = envvars_getDefaultValidationEpsilon(); } qreal validateconfig_getEpsilon() { return global_validationEpsilon; @@ -4181,21 +4188,19 @@ void validate_tempAllocSucceeded(bool succeeded, qindex numElems, qindex numByte * ENVIRONMENT VARIABLES */ -void validate_envVarIsBoolean(string varName, const char* varStr, const char* caller) { +void validate_envVarPermitNodesToShareGpu(string varValue, const char* caller) { - // empty non-compulsory environment vars never reach this validation function - assertThat(!parser_isStrEmpty(varStr), report::COMPULSORY_ENV_VAR_WAS_NOT_SPECIFIED_OR_EMPTY, caller); + // though caller should gaurantee varValue contains at least one character, + // we'll still check to avoid a segfault if this gaurantee is broken + bool isValid = (varValue.size() == 1) && (varValue[0] == '0' || varValue[0] == '1'); + assertThat(isValid, report::INVALID_PERMIT_NODES_TO_SHARE_GPU_ENV_VAR, caller); +} - // value must be a single 0 or 1 character (below expr works even when str has no terminal) - bool isValid = (varStr[0] == '0' || varStr[0] == '1') && (varStr[1] == '\0'); +void validate_envVarDefaultValidationEpsilon(string varValue, const char* caller) { - /// @todo include 'varName' in printed vars once tokenSubs can support strings - // hackily ensure "PERMIT_NODES_TO_SHARE_GPU" is featured in the error message as - // the only currently supported environment variable and is important to specify - string errMsg = (varName == "PERMIT_NODES_TO_SHARE_GPU")? - report::INVALID_PERMIT_NODES_TO_SHARE_GPU_ENV_VAR : - report::INVALID_BOOLEAN_ENVIRONMENT_VARIABLE; + assertThat(parser_isAnySizedReal(varValue), report::DEFAULT_EPSILON_ENV_VAR_NOT_A_REAL, caller); + assertThat(parser_isValidReal(varValue), report::DEFAULT_EPSILON_ENV_VAR_EXCEEDS_QREAL_RANGE, caller); - /// @todo include 'varStr' in printed vars once tokenSubs can support strings - assertThat(isValid, errMsg, caller); + qreal eps = parser_parseReal(varValue); + assertThat(eps >= 0, report::DEFAULT_EPSILON_ENV_VAR_IS_NEGATIVE, caller); } diff --git a/quest/src/core/validation.hpp b/quest/src/core/validation.hpp index cf8b62535..65afa9eff 100644 --- a/quest/src/core/validation.hpp +++ b/quest/src/core/validation.hpp @@ -514,7 +514,9 @@ void validate_tempAllocSucceeded(bool succeeded, qindex numElems, qindex numByte * ENVIRONMENT VARIABLES */ -void validate_envVarIsBoolean(std::string varName, const char* varStr, const char* caller); +void validate_envVarPermitNodesToShareGpu(string varValue, const char* caller); + +void validate_envVarDefaultValidationEpsilon(string varValue, const char* caller); diff --git a/quest/src/gpu/gpu_cuquantum.cuh b/quest/src/gpu/gpu_cuquantum.cuh index 9f80881a1..3b1c55fdb 100644 --- a/quest/src/gpu/gpu_cuquantum.cuh +++ b/quest/src/gpu/gpu_cuquantum.cuh @@ -134,9 +134,10 @@ int deallocMemInPool(void* ctx, void* ptr, size_t size, cudaStream_t stream) { void gpu_initCuQuantum() { // the cuStateVec docs say custatevecCreate() should be called - // once per physical GPU, though oversubscribing MPI processes - // while setting PERMIT_NODES_TO_SHARE_GPU=1 worked fine in our - // testing - we will treat it as tolerable but undefined behaviour + // once per physical GPU, though assigning multiple MPI processes + // to each GPU with each calling custatevecCreate() below worked + // fine in our testing. We here tolerate oversubscription, letting + // prior validation prevent it (disabled by an environment variable) // create new stream and cuQuantum handle, binding to global config CUDA_CHECK( custatevecCreate(&config.handle) ); From da403d65ba2ca2160e25ba89432a78e0b5ae2dc9 Mon Sep 17 00:00:00 2001 From: Tyson Jones Date: Mon, 23 Jun 2025 13:21:47 +0200 Subject: [PATCH 5/8] added env-vars to doc --- docs/launch.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/launch.md b/docs/launch.md index aadd02ca7..44a0f7fd7 100644 --- a/docs/launch.md +++ b/docs/launch.md @@ -22,6 +22,7 @@ Launching your [compiled](compile.md) QuEST application can be as straightforwar > - Tests > * v4 > * v3 +> - Configuring > - Multithreading > * Choosing threads > * Monitoring utilisation @@ -29,11 +30,11 @@ Launching your [compiled](compile.md) QuEST application can be as straightforwar > - GPU-acceleration > * Launching > * Monitoring -> * Configuring +> * Configuring > * Benchmarking > - Distribution > * Launching -> * Configuring +> * Configuring > * Benchmarking > - Multi-GPU > - Supercomputers @@ -243,6 +244,21 @@ ctest +--------------------- + + + + +## Configuring + +QuEST execution can be configured prior to runtime using the below [environment variables](https://en.wikipedia.org/wiki/Environment_variable). + +- [`PERMIT_NODES_TO_SHARE_GPU`](https://quest-kit.github.io/QuEST/group__modes.html#ga7e12922138caa68ddaa6221e40f62dda) +- [`DEFAULT_VALIDATION_EPSILON`](https://quest-kit.github.io/QuEST/group__modes.html#ga55810d6f3d23de810cd9b12a2bbb8cc2) + + + + --------------------- @@ -429,7 +445,7 @@ Usage of GPU-acceleration can be (inadvisably) forced using [`createForcedQureg( - + ### Configuring @@ -514,7 +530,7 @@ mpirun -np 1024 --oversubscribe ./mytests - + ### Configuring From 28d5c66faa4ead0764fc39db94acab87cb1f1ad3 Mon Sep 17 00:00:00 2001 From: Tyson Jones Date: Mon, 23 Jun 2025 13:22:12 +0200 Subject: [PATCH 6/8] made validation more specific --- quest/src/core/parser.cpp | 8 ++++---- quest/src/core/validation.cpp | 12 +++++++----- quest/src/core/validation.hpp | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/quest/src/core/parser.cpp b/quest/src/core/parser.cpp index 758cff64e..8884acc4c 100644 --- a/quest/src/core/parser.cpp +++ b/quest/src/core/parser.cpp @@ -202,7 +202,7 @@ bool parser_isAnySizedReal(string str) { // EXCEPT strings which contain a number too large to store in the qreal // type (as is separately checked below). Note it is insufficient to merely // duck-type using stold() et al because such functions permit non-numerical - // characters to follow the contained number (grr!) + // characters to follow the contained number which are silently removed (grr!) smatch match; return regex_match(str, match, regexes::real); } @@ -351,7 +351,7 @@ bool isInterpretablePauliStrSumLine(string line) { } -bool isCoeffValidInPauliStrSumLine(string line) { +bool isPauliStrSumCoeffWithinQcompRange(string line) { // it is gauranteed that line is interpretable and contains a regex-matching // coefficient, but we must additionally verify it is within range of qreal. @@ -414,8 +414,8 @@ void assertStringIsValidPauliStrSum(string lines, const char* caller) { validate_parsedPauliStrSumLineIsInterpretable(validLine, line, lineIndex, caller); // assert the coeff is parsable (e.g. doesn't exceed valid number range) - bool validCoeff = isCoeffValidInPauliStrSumLine(line); - validate_parsedPauliStrSumCoeffIsValid(validCoeff, line, lineIndex, caller); + bool validCoeff = isPauliStrSumCoeffWithinQcompRange(line); + validate_parsedPauliStrSumCoeffWithinQcompRange(validCoeff, line, lineIndex, caller); // assert the line has a consistent number of Paulis as previous int numLinePaulis = getNumPaulisInLine(line); diff --git a/quest/src/core/validation.cpp b/quest/src/core/validation.cpp index c5d0007a7..3f242e6df 100644 --- a/quest/src/core/validation.cpp +++ b/quest/src/core/validation.cpp @@ -710,8 +710,8 @@ namespace report { string PARSED_PAULI_STR_SUM_INCONSISTENT_NUM_PAULIS_IN_LINE = "Line ${LINE_NUMBER} specified ${NUM_LINE_PAULIS} Pauli operators which is inconsistent with the number of Paulis of the previous lines (${NUM_PAULIS})."; - string PARSED_PAULI_STR_SUM_COEFF_IS_INVALID = - "The coefficient of line ${LINE_NUMBER} could not be converted to a qcomp, possibly due to it exceeding the valid numerical range."; + string PARSED_PAULI_STR_SUM_COEFF_EXCEEDS_QCOMP_RANGE = + "The coefficient of line ${LINE_NUMBER} is a valid floating-point number but exceeds the range which can be stored in a qcomp. Consider increasing FLOAT_PRECISION."; string PARSED_STRING_IS_EMPTY = "The given string was empty (contained only whitespace characters) and could not be parsed."; @@ -1173,7 +1173,9 @@ qreal REDUCTION_EPSILON_FACTOR = 100; // the default epsilon is not known until runtime since the macro // UNSPECIFIED_DEFAULT_VALIDATION_EPSILON may be overriden by the -// DEFAULT_VALIDATION_EPSILON environment variable +// DEFAULT_VALIDATION_EPSILON environment variable. We do not read +// the env-var immediately since it may malformed; we must wait for +// initQuESTEnv() to validate and potentially throw an error static qreal global_validationEpsilon = -1; // must be overriden void validateconfig_setEpsilon(qreal eps) { @@ -3250,12 +3252,12 @@ void validate_parsedPauliStrSumLineHasConsistentNumPaulis(int numPaulis, int num assertThat(numPaulis == numLinePaulis, report::PARSED_PAULI_STR_SUM_INCONSISTENT_NUM_PAULIS_IN_LINE, vars, caller); } -void validate_parsedPauliStrSumCoeffIsValid(bool isCoeffValid, string line, qindex lineIndex, const char* caller) { +void validate_parsedPauliStrSumCoeffWithinQcompRange(bool isCoeffValid, string line, qindex lineIndex, const char* caller) { /// @todo we cannot yet report 'line' because tokenSubs so far only accepts integers :( tokenSubs vars = {{"${LINE_NUMBER}", lineIndex + 1}}; // lines begin at 1 - assertThat(isCoeffValid, report::PARSED_PAULI_STR_SUM_COEFF_IS_INVALID, vars, caller); + assertThat(isCoeffValid, report::PARSED_PAULI_STR_SUM_COEFF_EXCEEDS_QCOMP_RANGE, vars, caller); } void validate_parsedStringIsNotEmpty(bool stringIsNotEmpty, const char* caller) { diff --git a/quest/src/core/validation.hpp b/quest/src/core/validation.hpp index 65afa9eff..92baac843 100644 --- a/quest/src/core/validation.hpp +++ b/quest/src/core/validation.hpp @@ -312,7 +312,7 @@ void validate_newPauliStrSumAllocs(PauliStrSum sum, qindex numBytesStrings, qind void validate_parsedPauliStrSumLineIsInterpretable(bool isInterpretable, string line, qindex lineIndex, const char* caller); -void validate_parsedPauliStrSumCoeffIsValid(bool isCoeffValid, string line, qindex lineIndex, const char* caller); +void validate_parsedPauliStrSumCoeffWithinQcompRange(bool isCoeffValid, string line, qindex lineIndex, const char* caller); void validate_parsedPauliStrSumLineHasConsistentNumPaulis(int numPaulis, int numLinePaulis, string line, qindex lineIndex, const char* caller); From c0240d210f85314f487dfc033b55545fd4d60edb Mon Sep 17 00:00:00 2001 From: Tyson Jones Date: Mon, 23 Jun 2025 13:23:21 +0200 Subject: [PATCH 7/8] patched tests since float underflow is now detected - confirming the bug (qcomp literals underflowing to zero without a validation message) was silently occurring in our tests! --- tests/unit/paulis.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/unit/paulis.cpp b/tests/unit/paulis.cpp index f18b57228..255d30c4b 100644 --- a/tests/unit/paulis.cpp +++ b/tests/unit/paulis.cpp @@ -362,8 +362,9 @@ TEST_CASE( "createInlinePauliStrSum", TEST_CATEGORY ) { SECTION( "coefficient parsing" ) { - vector strs = {"1 X", "0 X", "0.1 X", "5E2-1i X", "-1E-50i X", "1 - 6E-5i X", "-1.5E-15 - 5.123E-30i 0"}; - vector coeffs = { 1, 0, 0.1, 5E2-1_i, -(1E-50)*1_i, 1 -(6E-5)*1_i, qcomp(-1.5E-15, -5.123E-30) }; + // beware that when FLOAT_PRECISION=1, qcomp cannot store smaller than 1E-37 (triggering a validation error) + vector strs = {"1 X", "0 X", "0.1 X", "5E2-1i X", "-1E-25i X", "1 - 6E-5i X", "-1.5E-15 - 5.123E-30i 0"}; + vector coeffs = { 1, 0, 0.1, 5E2-1_i, -(1E-25)*1_i, 1 -(6E-5)*1_i, qcomp(-1.5E-15, -5.123E-30) }; size_t i = GENERATE_REF( range(0, (int) strs.size()) ); CAPTURE( strs[i], coeffs[i] ); @@ -377,7 +378,7 @@ TEST_CASE( "createInlinePauliStrSum", TEST_CATEGORY ) { PauliStrSum sum = createInlinePauliStrSum(R"( + 5E2-1i XYZ - - 1E-50i IXY + - 1E-20i IXY + 1 - 6E-5i IIX 0 III 5. XXX @@ -416,6 +417,12 @@ TEST_CASE( "createInlinePauliStrSum", TEST_CATEGORY ) { REQUIRE_NOTHROW( createInlinePauliStrSum("1 2 3") ); // = 1 * YZ and is legal } + SECTION( "out of range" ) { + + // the max/min qcomp depend upon FLOAT_PRECISION but we'll lazily use something even quad-prec cannot store + REQUIRE_THROWS_WITH( createInlinePauliStrSum("-1E-9999 XYZ"), ContainsSubstring("exceeds the range which can be stored in a qcomp") ); + } + SECTION( "inconsistent number of qubits" ) { REQUIRE_THROWS_WITH( createInlinePauliStrSum("3 XYZ \n 2 YX"), ContainsSubstring("inconsistent") ); @@ -444,7 +451,7 @@ TEST_CASE( "createPauliStrSumFromFile", TEST_CATEGORY ) { file.open(fn); file << R"( + 5E2-1i XYZ - - 1E-50i IXY + - 1E-20i IXY + 1 - 6E-5i IIX 0 III 5. IXX @@ -497,7 +504,7 @@ TEST_CASE( "createPauliStrSumFromReversedFile", TEST_CATEGORY ) { file.open(fn); file << R"( + 5E2-1i XYZ - - 1E-50i IXY + - 1E-20i IXY + 1 - 6E-5i IIX 0 III 5. IXX From f08c418985aa6a889c29d63004b1d3c7b68ee6ed Mon Sep 17 00:00:00 2001 From: Tyson Jones Date: Wed, 25 Jun 2025 22:57:08 +0200 Subject: [PATCH 8/8] adding missing guards and doxy cmd --- quest/include/modes.h | 4 ++-- quest/src/core/envvars.hpp | 6 ++++++ utils/docs/Doxyfile | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/quest/include/modes.h b/quest/include/modes.h index f7d53c3c8..7633d2bca 100644 --- a/quest/include/modes.h +++ b/quest/include/modes.h @@ -127,7 +127,7 @@ * @warning * Permitting GPU sharing may cause unintended behaviour when additionally using cuQuantum. * - * @par Values + * @envvarvalues * - forbid sharing: @p 0, @p '0', @p '', @p , (unspecified) * - permit sharing: @p 1, @p '1' * @@ -146,7 +146,7 @@ * unless overriden at runtime via setValidationEpsilon(), in which case it can be * restored to that specified by this environment variable using setValidationEpsilonToDefault(). * - * @par Values + * @envvarvalues * - setting @p DEFAULT_VALIDATION_EPSILON=0 disables numerical validation, as if the value * were instead infinity. * - setting @p DEFAULT_VALIDATION_EPSILON='' is equivalent to _not_ specifying the variable, diff --git a/quest/src/core/envvars.hpp b/quest/src/core/envvars.hpp index 13380ffa0..828d5605e 100644 --- a/quest/src/core/envvars.hpp +++ b/quest/src/core/envvars.hpp @@ -6,6 +6,9 @@ * @author Tyson Jones */ +#ifndef ENVVARS_HPP +#define ENVVARS_HPP + #include @@ -29,3 +32,6 @@ void envvars_validateAndLoadEnvVars(const char* caller); bool envvars_getWhetherGpuSharingIsPermitted(); qreal envvars_getDefaultValidationEpsilon(); + + +#endif // ENVVARS_HPP diff --git a/utils/docs/Doxyfile b/utils/docs/Doxyfile index f113eaba5..daa1f0143 100644 --- a/utils/docs/Doxyfile +++ b/utils/docs/Doxyfile @@ -302,6 +302,7 @@ ALIASES += "cpponly=@remark This function is only available in C++." ALIASES += "conly=@remark This function is only available in C." ALIASES += "macrodoc=@note This entity is actually a macro." ALIASES += "envvardoc=@note This entity is actually an environment variable." +ALIASES += "envvarvalues=@par Values" ALIASES += "neverdoced=@warning This entity is a macro, undocumented directly due to a Doxygen limitation. If you see this doc rendered, contact the devs!" ALIASES += "myexample=@par Example" ALIASES += "equivalences=@par Equivalences"