Skip to content

Commit 702e2db

Browse files
committed
Use cublas for 1x1 matmuls, not 1x1 convs
1 parent 9c38e44 commit 702e2db

2 files changed

Lines changed: 88 additions & 5 deletions

File tree

cpp/configs/gtp_example.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,9 @@ searchFactorWhenWinningThreshold = 0.95
454454
# you can uncomment these lines and change them to "true" or "false".
455455
# cudaUseFP16 = auto
456456
# cudaUseNHWC = auto
457+
# Whether 1x1 NHWC convolutions run as a cuBLAS GEMM instead of a cuDNN convolution.
458+
# "auto" (default) uses the GEMM only in FP16, where it is slightly faster.
459+
# cudaUse1x1Matmul = auto
457460

458461
# ------------------------------
459462
# Metal GPU settings

cpp/neuralnet/cudabackend.cpp

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ struct CudaHandles {
303303
// Set once we have logged that cudaDisableGraphSDPA actually suppressed an otherwise-usable SDPA path,
304304
// so the message is emitted only a single time per handle rather than on every attention block.
305305
bool loggedGraphSDPADisabled;
306+
// Controls whether 1x1 NHWC convs run as a cuBLAS GEMM (vs cuDNN). Auto = matmul iff FP16.
307+
// True/False force the choice regardless of precision.
308+
enabled_t use1x1MatmulMode;
306309

307310
CudaHandles(int major, int minor)
308311
: majorComputeCapability(major),
@@ -311,7 +314,8 @@ struct CudaHandles {
311314
logger(NULL),
312315
isWarmup(false),
313316
cudaDisableGraphSDPA(false),
314-
loggedGraphSDPADisabled(false)
317+
loggedGraphSDPADisabled(false),
318+
use1x1MatmulMode(enabled_t::Auto)
315319
{
316320
CUBLAS_ERR("CudaHandles",cublasCreate(&cublas));
317321
CUDNN_ERR("CudaHandles",cudnnCreate(&cudnn));
@@ -531,6 +535,13 @@ struct ConvLayer {
531535
ByBatchSize<cudnnConvolutionFwdAlgo_t>* convolutionAlgorithms; //array of one for each batch size
532536
#endif
533537
void* filterBuf;
538+
// A 1x1 conv is equivalent to a matmul. When use1x1Matmul is set we run it as a cuBLAS GEMM over
539+
// batch*spatial tokens and build NO cuDNN objects. This is the default for 1x1 NHWC FP16 convs.
540+
// matmulWeightBuf is [inC, outC] column-major (cuBLAS order); matmulSpatialSize is the spatial length.
541+
bool use1x1Matmul;
542+
int matmulSpatialSize;
543+
void* matmulWeightBuf;
544+
bool usingFP16;
534545

535546
ConvLayer() = delete;
536547
ConvLayer(const ConvLayer&) = delete;
@@ -567,6 +578,36 @@ struct ConvLayer {
567578
testAssert(convXSize % 2 == 1);
568579
testAssert(convYSize % 2 == 1);
569580

581+
usingFP16 = useFP16;
582+
filterBuf = NULL;
583+
matmulWeightBuf = NULL;
584+
filterDescriptor = NULL;
585+
convolutionDescriptor = NULL;
586+
convolutionAlgorithms = NULL;
587+
588+
// A 1x1 conv is a matmul, and cuBLAS is faster than cuDNN's conv in FP16 (tensor cores).
589+
// Benchmarked as slightly faster on convnets and neutral on transformers.
590+
// In FP32 there wasn't an improvement, so the default (cudaUse1x1Matmul=Auto) only uses GEMM in FP16.
591+
// The config flag can force it either way regardless of precision. Supports NHWC only (the GEMM assumes
592+
// channel-contiguous-per-position layout).
593+
use1x1Matmul = false;
594+
if(convXSize == 1 && convYSize == 1 && useNHWCIn && useNHWCOut) {
595+
enabled_t mode = cudaHandles->use1x1MatmulMode;
596+
use1x1Matmul = (mode == enabled_t::True) || (mode == enabled_t::Auto && useFP16);
597+
}
598+
matmulSpatialSize = use1x1Matmul ? (manager->nnYLen * manager->nnXLen) : 0;
599+
600+
if(use1x1Matmul) {
601+
// 1x1 conv weights are [outC, inC]. cuBLAS GEMM wants column-major, i.e. [inC, outC] in row-major notation.
602+
// So transpose. No cuDNN objects are built.
603+
vector<float> wT((size_t)inChannels * outChannels);
604+
for(int oc = 0; oc < outChannels; oc++)
605+
for(int ic = 0; ic < inChannels; ic++)
606+
wT[(size_t)oc + (size_t)ic * outChannels] = desc->weights[(size_t)oc * inChannels + ic];
607+
CudaUtils::mallocAndCopyToDevice(name + ":matmulW", wT, matmulWeightBuf, useFP16);
608+
return;
609+
}
610+
570611
inputDescriptors = manager->getTensorDesc4DByBatchSize(inChannels,useFP16,useNHWCIn);
571612
outputDescriptors = manager->getTensorDesc4DByBatchSize(outChannels,useFP16,useNHWCOut);
572613
int maxBatchSize = manager->maxBatchSize;
@@ -679,16 +720,22 @@ struct ConvLayer {
679720
}
680721

681722
~ConvLayer() {
682-
cudaFree(filterBuf);
683-
cudnnDestroyFilterDescriptor(filterDescriptor);
684-
cudnnDestroyConvolutionDescriptor(convolutionDescriptor);
685-
delete convolutionAlgorithms;
723+
if(matmulWeightBuf != NULL)
724+
cudaFree(matmulWeightBuf);
725+
if(!use1x1Matmul) {
726+
cudaFree(filterBuf);
727+
cudnnDestroyFilterDescriptor(filterDescriptor);
728+
cudnnDestroyConvolutionDescriptor(convolutionDescriptor);
729+
delete convolutionAlgorithms;
730+
}
686731
}
687732

688733
size_t requiredWorkspaceBytes(
689734
CudaHandles* cudaHandles,
690735
int batchSize
691736
) const {
737+
if(use1x1Matmul)
738+
return 0;
692739
size_t workspaceBytes = 0;
693740
#if CUDNN_MAJOR >= 8
694741
CUDNN_ERR(name.c_str(),cudnnGetConvolutionForwardWorkspaceSize(
@@ -723,6 +770,33 @@ struct ConvLayer {
723770
void* workspaceBuf,
724771
size_t workspaceBytes
725772
) const {
773+
if(use1x1Matmul) {
774+
// out[outC, tokens] = W[outC, inC] x in[inC, tokens]
775+
// where tokens = batchSize * spatial. NHWC buffers are [tokens, C] row-major = [C, tokens] column-major
776+
// matching cuBLAS's expectation. Same as MatMulLayer.
777+
int tokens = batchSize * matmulSpatialSize;
778+
if(!usingFP16) {
779+
const float alpha = 1.0f;
780+
const float beta = accumulate ? 1.0f : 0.0f;
781+
CUBLAS_ERR(name.c_str(),cublasSgemm(
782+
cudaHandles->cublas, CUBLAS_OP_N, CUBLAS_OP_N,
783+
outChannels, tokens, inChannels,
784+
&alpha, (const float*)matmulWeightBuf, outChannels,
785+
(const float*)inputBuf, inChannels,
786+
&beta, (float*)outputBuf, outChannels));
787+
}
788+
else {
789+
const half alpha = __float2half(1.0f);
790+
const half beta = __float2half(accumulate ? 1.0f : 0.0f);
791+
CUBLAS_ERR(name.c_str(),cublasHgemm(
792+
cudaHandles->cublas, CUBLAS_OP_N, CUBLAS_OP_N,
793+
outChannels, tokens, inChannels,
794+
&alpha, (const half*)matmulWeightBuf, outChannels,
795+
(const half*)inputBuf, inChannels,
796+
&beta, (half*)outputBuf, outChannels));
797+
}
798+
return;
799+
}
726800
const float alpha = 1.0f;
727801
const float beta = accumulate ? 1.0f : 0.0f;
728802
#if CUDNN_MAJOR >= 8
@@ -3231,6 +3305,8 @@ struct ComputeContext {
32313305
enabled_t useNHWCMode;
32323306
// If true, skip the cudnn graph SDPA path entirely and always use the custom attention kernel.
32333307
bool cudaDisableGraphSDPA;
3308+
// Whether 1x1 NHWC convs use the cuBLAS GEMM path. Auto = matmul iff FP16.
3309+
enabled_t use1x1MatmulMode;
32343310
};
32353311

32363312
ComputeContext* NeuralNet::createComputeContext(
@@ -3258,6 +3334,8 @@ ComputeContext* NeuralNet::createComputeContext(
32583334
cfg.contains("cudaUseNHWC") ? cfg.getEnabled("cudaUseNHWC") : enabled_t::Auto;
32593335
context->cudaDisableGraphSDPA =
32603336
cfg.contains("cudaDisableGraphSDPA") ? cfg.getBool("cudaDisableGraphSDPA") : false;
3337+
context->use1x1MatmulMode =
3338+
cfg.contains("cudaUse1x1Matmul") ? cfg.getEnabled("cudaUse1x1Matmul") : enabled_t::Auto;
32613339
return context;
32623340
}
32633341

@@ -3298,6 +3376,8 @@ struct ComputeHandle {
32983376
usingNHWC(useNHWC)
32993377
{
33003378
cudaHandles = std::make_unique<CudaHandles>(majorComputeCapability,minorComputeCapability);
3379+
// Must be set before building the model: ConvLayer reads it at construction to pick the 1x1 conv path.
3380+
cudaHandles->use1x1MatmulMode = context->use1x1MatmulMode;
33013381
model = std::make_unique<Model>(
33023382
cudaHandles.get(), &(loadedModel->modelDesc), maxBatchSize,
33033383
nnXLen, nnYLen, inputsUseNHWC, useFP16, useNHWC

0 commit comments

Comments
 (0)