Skip to content

Set gpu tpb#736

Draft
otbrown wants to merge 5 commits into
develfrom
set_gpu_tpb
Draft

Set gpu tpb#736
otbrown wants to merge 5 commits into
develfrom
set_gpu_tpb

Conversation

@otbrown
Copy link
Copy Markdown
Collaborator

@otbrown otbrown commented Apr 24, 2026

Creating a facility for users to runtime set threads per block for tuning the GPU implementation. NOTE: only applies to kernels that are not handled by Thrust, which does its own thing. Resolves #735.

I considered and rejected the idea of creating a symmetric interface for the CPU for users who don't know OMP_NUM_THREADS or omp_set_num_threads() exist, but that's much riskier as the point of truth is external (in the OpenMP runtime).

TODO:

  • Should gpu_getNumThreadsPerBlock return a qindex? Probably.
  • Create a new home for user facing API, as environment doesn't really make sense.
  • Add a compile time default value -- that way expert maintainers can compile a tuned default into a library which is used on a system.
  • Query seemingly unused branch at
    if constexpr (NumTargs != -1) {
  • Add TPB to QuEST GPU environment reporting.
  • Add tests for new interface.
  • @JPRichings To check if this is really worthwhile, but please wait a week to tell me if it isn't.
    • It is!

@otbrown
Copy link
Copy Markdown
Collaborator Author

otbrown commented Apr 24, 2026

Rudimentary testing done with:

#include <cstdio>
#include "quest.h"

int main (void)
{
  const int NQUBITS = 24;
  const int TPB = 32;


  initQuESTEnv();
  reportQuESTEnv();

  std::printf("Initial number of threads per block: %d\n", getQuESTGpuThreadsPerBlock());

  setQuESTGpuThreadsPerBlock(TPB);
  std::printf("New number of threads per block: %d\n", getQuESTGpuThreadsPerBlock());

  Qureg qureg = createForcedQureg(NQUBITS);

  std::printf("Initialising Qureg.\n");
  initPlusState(qureg);
  reportQureg(qureg);

  std::printf("Applying Quantum Fourier Transform.\n");
  applyFullQuantumFourierTransform(qureg, false);
  reportQureg(qureg);

  destroyQureg(qureg);
  finalizeQuESTEnv();

  return 0;
}

@otbrown otbrown self-assigned this Apr 24, 2026
@JPRichings
Copy link
Copy Markdown
Contributor

Why would gpu_getNumThreadsPerBlock be a qindex this is not a quantum quantity. uint should be fine ( I am sure there is a recommendation from the cuda api we can match.

@TysonRayJones
Copy link
Copy Markdown
Member

Is there an advantage to users having to set this as a runtime hyperparameter? My (mostly undeveloped) belief is we can use occupancy tools (alluded to here) to automate this. I definitely shy from giving users a greater onus to optimise for their settings (like other prolific softwares), which the v4 overhaul was supposed to avoid (via e.g. the autodeployer).

Note too that the kernels so far are very primitive - each thread handles the updating of the minimum possible number of amplitudes (often just one!). I quite like that because it's very readable and simple (great for an open-source scientific project) but is an obvious site for optimisation.

Why would gpu_getNumThreadsPerBlock be a qindex this is not a quantum quantity. uint should be fine ( I am sure there is a recommendation from the cuda api we can match.

It's true that it will never be anywhere as big as the quantities qindex is expected to store (like the number of basis states), but I have already used it in places where I thought an int might be insufficient. Inoffensive either way as uint or qindex imo

@JPRichings
Copy link
Copy Markdown
Contributor

Hi Tyson,

I just noticed the fixed value to 128 and have a feeling that it was large.

I just wanted a handle so I could write a benchmark so we can easily automate performance tuning ourselves.

I have not played with the occupancy tools but I should take a proper look as this might solve this automatically.

My other concern is that there are differences between Nvidia and AMD on optimal sizes due to hardware differences so we might not be able to reply on the occupancy tuning in all cases unless this becomes available on all platforms.

Comment thread quest/src/cpu/cpu_config.cpp
Comment thread quest/src/gpu/gpu_subroutines.cpp Outdated

qindex numThreads = qureg.numAmpsPerNode / powerOf2(qubits.size());
qindex numBlocks = getNumBlocks(numThreads);
const int NUM_THREADS_PER_BLOCK = gpu_getNumThreadsPerBlock();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we opt for this, why is NUM_THREADS_PER_BLOCK capitalised like a constant? It's runtime

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree capitalisation here bad.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's const in scope 😉 apologies, accidentally following my own style guide there rather than the QuEST one. I'll %s/NUM_THREADS_PER_BLOCK/numThreadsPerBlock/g it.

@TysonRayJones
Copy link
Copy Markdown
Member

I just noticed the fixed value to 128 and have a feeling that it was large.

I guess it's very GPU specific! I think 128 was motivated by thinking of CC=3, which has a max active blocks per SM of 16, and a max active threads per SM of 2048. So using 128 threads per block perfectly maximally occupies the SMs (when there are enough amplitudes to admit more than 16 blocks per SM, of course!)

For illustration, the next smallest size is 96 (it must be a multiple of 32, else threads within a warp will be idle), which yields a number of active threads of 16 * 96 = 1536, which wastes 2048 - 1536 = 512 threads per SM!

Of course, newer GPUs support more active blocks per SM (even when the max active threads per SM is unchanged). E.g. CC=8 supports up to 32 active blocks per SM, so we could shrink to 64 threads per block while achieving the same occupancy - but I don't have a great intuition for the effect when we're memory-bandwidth bound.

Certainly seems prudent to consult a CUDA runtime API, if that doesn't hurt our AMD compatibility!

@otbrown
Copy link
Copy Markdown
Collaborator Author

otbrown commented Apr 27, 2026

Apologies, probably won't get to look at this again this week, but very happy to set this value programmatically if it can be done!

As it's architecture dependent, we definitely do need a way to adjust it, and ideally both at runtime and compile time. At compile time, so kindly HPC support teams can compile and maintain a tuned version, and at runtime, so they can scan through values without having to recompile in between. I'll have a chat with James abour approaches later this week!

I 100% agree that we don't really want unknowing users messing around with this. I think something like an architecture.h or perftune.h or similar might be the best solution. A set of functionality that we explicitly document is for users who know what they are doing to tune the performance of the library for a specific architecture. It might be this is the only value in there for the time being, but for slingshot-11 reasons we need to add a parameter capping the total in-flight data and this would be a good spot for that too.

@TysonRayJones
Copy link
Copy Markdown
Member

Fair enough - you've convinced me! Being able to runtime adjust is of course extremely helping during development of a user-friendlier adaptive system anyhow.

I like the sound of perftune.h - it could also go into debug.h in the interim to there being more performance-tuning specific functionality.

}

void setQuESTGpuThreadsPerBlock(const int NEW_TPB) {
// just rely on the internal function to throw an error if there's no GPU support compiled
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: validate this is a factor of 32 (and is positive, etc etc)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc to user: HIP warpsize is 64!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a better alternative: add gpu_isHipCompiled() in gpu_config.cpp, right under gpu_isCuQuantumCompiled(), as:

bool gpu_isHipCompiled() {
    return (bool) (COMPILE_CUDA && defined(__HIP__));
}

Then we can validate explicitly that when GPU-accelerated and we're on HIP, arg must be a multiple of 64, else of 32. This means 32 is required even when not GPU-accelerated; so we make that error message:

The number of threads per block must be a multiple of 32 (or on AMD GPUs, 64)

@otbrown
Copy link
Copy Markdown
Collaborator Author

otbrown commented May 3, 2026

Should validate TPB is multiple of 32!



int getQuESTGpuThreadsPerBlock() {
QuESTEnv env = getQuESTEnv();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note getQuESTEnv() is an API function with its own validation, and shouldn't be called internally like this since if its validation throws, it will claim the user called getQuESTEnv(), when they actually called getQuESTGpuThreadsPerBlock().

Should therefore first call

    validate_envIsInit(__func__);

and subsequently use globalEnvPtr directly.

(I see getEnvironmentString() calls getQuESTEnv() for some reason, when it too should just use globalEnvPtr)

* This is somehow probably the best pre-existing place for this. It only really applies to GPU, because for
* OpenMP the user can just export OMP_NUM_THREADS or call omp_set_num_threads.
*/
int getQuESTGpuThreadsPerBlock();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include Num somewhere, e.g.

  • getNumQuESTGpuThreadsPerBlock
  • getQuESTNumGpuThreadsPerBlock

I've so far tried to avoid abbreviating where feasible.

* OpenMP the user can just export OMP_NUM_THREADS or call omp_set_num_threads.
*/
int getQuESTGpuThreadsPerBlock();
void setQuESTGpuThreadsPerBlock(const int NEW_TPB);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Same Num) consideration as for getQuESTGpuThreadsPerBlock)

}

void setQuESTGpuThreadsPerBlock(const int NEW_TPB) {
// just rely on the internal function to throw an error if there's no GPU support compiled
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also call

validate_envIsInit(__func__);


int getQuESTGpuThreadsPerBlock() {
QuESTEnv env = getQuESTEnv();
return env.isGpuAccelerated? gpu_getNumThreadsPerBlock() : 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think this is a pitfall. If setQuESTGpuThreadsPerBlock() is permitted in non-GPU mode (and I really believe it should for healthy platform agnosticism), then the user would always get back 0 in lieu of what they had just passed to set. Maybe we should just always return gpu_getNumThreadsPerBlock().

The situation is slightly different to the GPU cache (fetchable by getGpuCacheSize() and clearable via clearGpuCache()), because that offers no setter. Users can always safely call both in non-GPU accelerated mode, and the former will return 0 (which is always correct).

return env.isGpuAccelerated? gpu_getNumThreadsPerBlock() : 0;
}

void setQuESTGpuThreadsPerBlock(const int NEW_TPB) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEW_TPB -> numThreadsPerBlock or numTPB, etc

#include "quest/src/gpu/cuda_to_hip.hpp"
#endif

int numThreadsPerBlock = 128;
Copy link
Copy Markdown
Member

@TysonRayJones TysonRayJones May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should give this a global_ prefix, like here (like I failed to do for hasGpuBeenBound, oops!)

Further, given it's not accessed anywhere outside gpu_(g|s)etNumThreadsPerBlock(), I would move this definition to the ENVIRONMENT MANAGEMENT section, just before gpu_getNumThreadsPerBlock().

error_gpuQueriedButGpuNotCompiled();
#endif
return;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we permit users to call the corresponding API functions when GPU acceleration is not enabled, then these guards can be removed entirely. I think that's fair/natural, because we certainly shouldn't introduce an API difference between compiling but not running with GPU acceleration.

I would also comment this exception. So this could become:

int gpu_getNumThreadsPerBlock() {
    // permitted even when GPU backend not compiled
    return globlal_numThreadsPerBlock;
}

void gpu_setNumThreadsPerBlock(const int newThreadsPerBlock) {
    // permitted even when GPU backend not compiled
    global_numThreadsPerBlock = newThreadsPerBlock;
}



__host__ qindex getNumBlocks(qindex numThreads) {
__host__ qindex getNumBlocks(qindex numThreads, const int numThreadsPerBlock) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove the const qualifier since it's asymmetric and a bit counterproductive, because it makes the reader why why isn't numThreads const


qindex numThreads = qureg.numAmpsPerNode / powerOf2(qubits.size());
qindex numBlocks = getNumBlocks(numThreads);
const int numThreadsPerBlock = gpu_getNumThreadsPerBlock();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the const before numThreadsPerBlock in gpu_subroutines.cpp are superfluous and only cause reader confusion (they might incorrectly infer that only the 2nd arg of <<< needs to be const, or something). Context already make the constness obvious

@otbrown
Copy link
Copy Markdown
Collaborator Author

otbrown commented May 19, 2026

Rather than attempt to post a thumbs up on each comment while on train WiFi, I'll just comment thanks @TysonRayJones here! I'm hoping to give this branch some proper attention on Thursday/Friday.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants