@@ -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
32363312ComputeContext* 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