@@ -1463,7 +1463,7 @@ public void CudaMoEExpertFFNDecode_RejectsInvalidRouterBeforeLaunch(
14631463 Assert . False ( CudaFusedOps . TryMoEExpertFFNDecode (
14641464 logits , moeInput , output , selected , routingWeights , gateUp , hidden ,
14651465 IntPtr . Zero , sentinel , sentinel ,
1466- quantType : 2 , numExperts , nUsed , hiddenDim : 32 , nFf : 32 ) ) ;
1466+ gateUpQuantType : 2 , downQuantType : 2 , numExperts , nUsed , hiddenDim : 32 , nFf : 32 ) ) ;
14671467 }
14681468
14691469 [ Fact ]
@@ -2933,6 +2933,157 @@ public void CudaQuantizedMatmulAndRows_IQ4XSMatchNativeReferenceAfterHostRelease
29332933
29342934 // Builds a byte-valid IQ4_XS weight buffer (any bit pattern is a legal block).
29352935 // block_iq4_xs = d(half) @0, scales_h(uint16) @2, scales_l[4] @4, qs[128] @8 => 136 bytes / 256 elems.
2936+ [ Fact ]
2937+ public void CudaQuantizedMatmul_IQ4NLMatchesNativeReference ( )
2938+ {
2939+ if ( ! CudaBackend . IsAvailable ( ) )
2940+ return ;
2941+
2942+ // IQ4_NL (ggml type 20) shares IQ4_XS's non-linear codebook but uses a flat
2943+ // 18-byte / 32-element block with a single scale and no sub-block scales.
2944+ // "Unsloth dynamic" quants mix it with other types per projection (the
2945+ // gemma-4-26B UD-IQ4_XS MoE is IQ3_S gate_up + IQ4_NL down), so without
2946+ // device support those tensors stayed host-backed and every matmul
2947+ // dequantized on the CPU.
2948+ const int rows = 3 ;
2949+ const int inDim = 256 ; // multiple of the 32-element IQ4_NL block
2950+ const int outDim = 5 ;
2951+ byte [ ] weights = CreateIq4NlRows ( outDim , inDim ) ;
2952+ float [ , ] input = new float [ rows , inDim ] ;
2953+ for ( int r = 0 ; r < rows ; r ++ )
2954+ for ( int c = 0 ; c < inDim ; c ++ )
2955+ input [ r , c ] = MathF . Sin ( ( r + 1 ) * ( c + 1 ) * 0.013f ) + MathF . Cos ( ( r + 2 ) * ( c + 3 ) * 0.007f ) * 0.3f ;
2956+
2957+ IntPtr host = Marshal . AllocHGlobal ( weights . Length ) ;
2958+ IntPtr cacheKey = new ( 0x767000 + ( int ) GgmlTensorType . IQ4_NL ) ;
2959+ try
2960+ {
2961+ Marshal . Copy ( weights , 0 , host , weights . Length ) ;
2962+ using var allocator = new CudaAllocator ( ) ;
2963+ Assert . True ( CudaQuantizedOps . SupportsQuantizedType ( ( int ) GgmlTensorType . IQ4_NL ) ) ;
2964+ CudaQuantizedOps . PreloadQuantizedWeight ( allocator , cacheKey , host , ( int ) GgmlTensorType . IQ4_NL , inDim , outDim , weights . Length ) ;
2965+
2966+ using var inputTensor = Tensor . FromArray ( allocator , input ) ;
2967+ using var output = new Tensor ( allocator , DType . Float32 , rows , outDim ) ;
2968+ Assert . True ( CudaQuantizedOps . TryAddmmQuantizedToFloat32 (
2969+ output ,
2970+ inputTensor ,
2971+ cacheKey ,
2972+ IntPtr . Zero ,
2973+ ( int ) GgmlTensorType . IQ4_NL ,
2974+ inDim ,
2975+ outDim ,
2976+ weights . Length ) ) ;
2977+
2978+ float [ ] expected = DequantizedMatmulNative ( weights , GgmlTensorType . IQ4_NL , outDim , inDim , input ) ;
2979+ float maxAbs = 0f ;
2980+ foreach ( float e in expected )
2981+ maxAbs = MathF . Max ( maxAbs , MathF . Abs ( e ) ) ;
2982+ AssertClose ( expected , output . GetElementsAsFloat ( rows * outDim ) , MathF . Max ( 5e-2f , maxAbs * 3e-4f ) ) ;
2983+ }
2984+ finally
2985+ {
2986+ Marshal . FreeHGlobal ( host ) ;
2987+ }
2988+ }
2989+
2990+ [ Fact ]
2991+ public void CudaQuantizedMatmul_MXFP4MatchesNativeReference ( )
2992+ {
2993+ if ( ! CudaBackend . IsAvailable ( ) )
2994+ return ;
2995+
2996+ // MXFP4 (ggml type 39) is the expert format of gpt-oss. Without device
2997+ // support its ffn_*_exps tensors stayed host-backed and every MoE matmul
2998+ // dequantized on the CPU (gpt-oss-20b decode measured 3.3 tok/s vs 150 on
2999+ // ggml_cuda). 17-byte block: one E8M0 shared exponent + 16 packed nibbles.
3000+ const int rows = 3 ;
3001+ const int inDim = 256 ; // multiple of the 32-element MXFP4 block
3002+ const int outDim = 5 ;
3003+ byte [ ] weights = CreateMxfp4Rows ( outDim , inDim ) ;
3004+ float [ , ] input = new float [ rows , inDim ] ;
3005+ for ( int r = 0 ; r < rows ; r ++ )
3006+ for ( int c = 0 ; c < inDim ; c ++ )
3007+ input [ r , c ] = MathF . Sin ( ( r + 1 ) * ( c + 1 ) * 0.017f ) + MathF . Cos ( ( r + 2 ) * ( c + 3 ) * 0.009f ) * 0.3f ;
3008+
3009+ IntPtr host = Marshal . AllocHGlobal ( weights . Length ) ;
3010+ IntPtr cacheKey = new ( 0x767000 + ( int ) GgmlTensorType . MXFP4 ) ;
3011+ try
3012+ {
3013+ Marshal . Copy ( weights , 0 , host , weights . Length ) ;
3014+ using var allocator = new CudaAllocator ( ) ;
3015+ Assert . True ( CudaQuantizedOps . SupportsQuantizedType ( ( int ) GgmlTensorType . MXFP4 ) ) ;
3016+ CudaQuantizedOps . PreloadQuantizedWeight ( allocator , cacheKey , host , ( int ) GgmlTensorType . MXFP4 , inDim , outDim , weights . Length ) ;
3017+
3018+ using var inputTensor = Tensor . FromArray ( allocator , input ) ;
3019+ using var output = new Tensor ( allocator , DType . Float32 , rows , outDim ) ;
3020+ Assert . True ( CudaQuantizedOps . TryAddmmQuantizedToFloat32 (
3021+ output ,
3022+ inputTensor ,
3023+ cacheKey ,
3024+ IntPtr . Zero ,
3025+ ( int ) GgmlTensorType . MXFP4 ,
3026+ inDim ,
3027+ outDim ,
3028+ weights . Length ) ) ;
3029+
3030+ float [ ] expected = DequantizedMatmulNative ( weights , GgmlTensorType . MXFP4 , outDim , inDim , input ) ;
3031+ float maxAbs = 0f ;
3032+ foreach ( float e in expected )
3033+ maxAbs = MathF . Max ( maxAbs , MathF . Abs ( e ) ) ;
3034+ AssertClose ( expected , output . GetElementsAsFloat ( rows * outDim ) , MathF . Max ( 5e-2f , maxAbs * 3e-4f ) ) ;
3035+ }
3036+ finally
3037+ {
3038+ Marshal . FreeHGlobal ( host ) ;
3039+ }
3040+ }
3041+
3042+ private static byte [ ] CreateMxfp4Rows ( int rows , int cols )
3043+ {
3044+ const int blockSize = 32 ;
3045+ const int blockBytes = 17 ; // E8M0 exponent byte + 16 packed nibble bytes
3046+ Assert . Equal ( 0 , cols % blockSize ) ;
3047+ int blocksPerRow = cols / blockSize ;
3048+ byte [ ] raw = new byte [ rows * blocksPerRow * blockBytes ] ;
3049+ for ( int r = 0 ; r < rows ; r ++ )
3050+ {
3051+ for ( int b = 0 ; b < blocksPerRow ; b ++ )
3052+ {
3053+ int offset = ( r * blocksPerRow + b ) * blockBytes ;
3054+ // Exponents around the 127 bias, including the e < 2 denormal cases
3055+ // that take ggml's special branch.
3056+ raw [ offset ] = ( byte ) ( ( r == 0 && b < 2 ) ? b : ( 120 + ( ( r * 3 + b ) % 14 ) ) ) ;
3057+ for ( int i = 0 ; i < 16 ; i ++ )
3058+ raw [ offset + 1 + i ] = ( byte ) ( ( r * 31 + b * 19 + i * 13 + 5 ) & 0xFF ) ;
3059+ }
3060+ }
3061+
3062+ return raw ;
3063+ }
3064+
3065+ private static byte [ ] CreateIq4NlRows ( int rows , int cols )
3066+ {
3067+ const int blockSize = 32 ;
3068+ const int blockBytes = 18 ; // half d + 16 packed nibble bytes
3069+ Assert . Equal ( 0 , cols % blockSize ) ;
3070+ int blocksPerRow = cols / blockSize ;
3071+ byte [ ] raw = new byte [ rows * blocksPerRow * blockBytes ] ;
3072+ for ( int r = 0 ; r < rows ; r ++ )
3073+ {
3074+ for ( int b = 0 ; b < blocksPerRow ; b ++ )
3075+ {
3076+ int offset = ( r * blocksPerRow + b ) * blockBytes ;
3077+ WriteHalf ( raw , offset , 0.0078125f + r * 0.001953125f + b * 0.0009765625f ) ;
3078+ // Deterministic nibble pattern covering the full 16-entry codebook.
3079+ for ( int i = 0 ; i < 16 ; i ++ )
3080+ raw [ offset + 2 + i ] = ( byte ) ( ( r * 29 + b * 17 + i * 11 + 7 ) & 0xFF ) ;
3081+ }
3082+ }
3083+
3084+ return raw ;
3085+ }
3086+
29363087 private static byte [ ] CreateIq4XsRows ( int rows , int cols )
29373088 {
29383089 const int blockSize = 256 ;
0 commit comments