Skip to content

Commit ec276c1

Browse files
authored
Merge pull request #126 from zhongkaifu/feature/bug_fix
bug fix
2 parents baece9d + 74f00e0 commit ec276c1

11 files changed

Lines changed: 19401 additions & 17582 deletions

File tree

InferenceWeb.Tests/CudaBackendTests.cs

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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;

TensorSharp.Backends.Cuda/CudaFusedOps.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,12 @@ public static bool TryMoEExpertFFNDecode(
626626
IntPtr perExpertScalePtr, // device [numExperts] F32 or IntPtr.Zero
627627
IntPtr gateUpPtrTable, // device [numExperts] u64
628628
IntPtr downPtrTable, // device [numExperts] u64
629-
int quantType, int numExperts, int nUsed, int hiddenDim, int nFf,
629+
// Gate+up and down may carry DIFFERENT quant types: "unsloth dynamic"
630+
// mixes them per projection (gemma-4-26B UD-IQ4_XS is IQ3_S gate_up +
631+
// IQ4_NL down). The gate_up and down kernels each take their own type,
632+
// so only this signature ever forced them to agree.
633+
int gateUpQuantType, int downQuantType,
634+
int numExperts, int nUsed, int hiddenDim, int nFf,
630635
// Q4_K dp4a fast path (see the ts_moe_expert_*_q4k_dp4a kernels): q8_1
631636
// scratch for the MoE input (moeInputQ8, hiddenDim/32 blocks) and the
632637
// GEGLU output (hAllQ8, nUsed * nFf/32 blocks). Both null -> the generic
@@ -641,7 +646,8 @@ public static bool TryMoEExpertFFNDecode(
641646
// while nUsed == 0 produces an invalid zero-height expert launch.
642647
if (!IsMoERouterConfigurationSupported(numExperts, nUsed)
643648
|| hiddenDim <= 0 || nFf <= 0 || nFf > int.MaxValue / 2
644-
|| !CudaQuantizedOps.SupportsQuantizedType(quantType))
649+
|| !CudaQuantizedOps.SupportsQuantizedType(gateUpQuantType)
650+
|| !CudaQuantizedOps.SupportsQuantizedType(downQuantType))
645651
{
646652
return false;
647653
}
@@ -699,7 +705,9 @@ public static bool TryMoEExpertFFNDecode(
699705
// Both dims are a multiple of 32 (block size) for these quants; falls
700706
// back to the generic kernels for any other quant type or missing
701707
// scratch.
702-
bool dp4a = useDp4a && (quantType == 2 || quantType == 12)
708+
static bool IsDp4aExpertType(int t) => t == 2 || t == 12;
709+
bool dp4a = useDp4a
710+
&& IsDp4aExpertType(gateUpQuantType) && IsDp4aExpertType(downQuantType)
703711
&& moeInputQ8?.Storage is CudaStorage && hAllQ8?.Storage is CudaStorage
704712
&& (hiddenDim & 31) == 0 && (nFf & 31) == 0;
705713

@@ -708,16 +716,16 @@ public static bool TryMoEExpertFFNDecode(
708716
IntPtr moeInQ8 = DeviceBufferOf(moeInputQ8);
709717
IntPtr hQ8 = DeviceBufferOf(hAllQ8);
710718
kernels.LaunchQuantizeQ81Rows(moeInPtr, moeInQ8, hiddenDim, 1, stream, warpCooperative: true);
711-
kernels.LaunchMoEExpertGateUpDp4a(gateUpPtrTable, selPtr, moeInQ8, guPtr, quantType, hiddenDim, twoNff, nUsed, stream);
719+
kernels.LaunchMoEExpertGateUpDp4a(gateUpPtrTable, selPtr, moeInQ8, guPtr, gateUpQuantType, hiddenDim, twoNff, nUsed, stream);
712720
kernels.LaunchGELUMulSplitF32(guPtr, hPtr, nUsed, nFf, stream);
713721
kernels.LaunchQuantizeQ81Rows(hPtr, hQ8, nFf, nUsed, stream, warpCooperative: true);
714-
kernels.LaunchMoEExpertDownDp4a(downPtrTable, selPtr, rwPtr, hQ8, outPtr, quantType, nFf, hiddenDim, nUsed, stream);
722+
kernels.LaunchMoEExpertDownDp4a(downPtrTable, selPtr, rwPtr, hQ8, outPtr, downQuantType, nFf, hiddenDim, nUsed, stream);
715723
}
716724
else
717725
{
718-
kernels.LaunchMoEExpertGateUpVecF32(gateUpPtrTable, selPtr, moeInPtr, guPtr, quantType, hiddenDim, twoNff, nUsed, stream);
726+
kernels.LaunchMoEExpertGateUpVecF32(gateUpPtrTable, selPtr, moeInPtr, guPtr, gateUpQuantType, hiddenDim, twoNff, nUsed, stream);
719727
kernels.LaunchGELUMulSplitF32(guPtr, hPtr, nUsed, nFf, stream);
720-
kernels.LaunchMoEExpertDownAccumF32(downPtrTable, selPtr, rwPtr, hPtr, outPtr, quantType, nFf, hiddenDim, nUsed, stream);
728+
kernels.LaunchMoEExpertDownAccumF32(downPtrTable, selPtr, rwPtr, hPtr, outPtr, downQuantType, nFf, hiddenDim, nUsed, stream);
721729
}
722730

723731
outStorage.MarkDeviceModified();

TensorSharp.Backends.Cuda/CudaP2PCommunicator.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,15 @@ private static bool[] EnablePeerAccess(CudaAllocator[] allocators)
102102
$" TP: P2P DMA self-test FAILED for GPU {i} → GPU {j} " +
103103
$"(cuDeviceCanAccessPeer=1 but data is corrupt). " +
104104
$"Falling back to host-staged transfers for this pair.");
105+
// Demote BOTH directions. A pair whose DMA is corrupt one
106+
// way is not trustworthy the other way either, and
107+
// CudaStorage.MarkPeerAccessFailed (used by every ordinary
108+
// cross-GPU tensor copy) already demotes the pair
109+
// symmetrically — leaving this table asymmetric would let a
110+
// collective peer-copy over a link that every other code
111+
// path has already given up on.
105112
enabled[i * n + j] = false;
113+
enabled[j * n + i] = false;
106114
CudaStorage.MarkPeerAccessFailed(
107115
allocators[i].DeviceId, allocators[j].DeviceId);
108116
}
@@ -186,6 +194,14 @@ private static unsafe bool VerifyP2PRoundTrip(CudaAllocator[] allocators, int sr
186194
}
187195
}
188196

197+
/// <summary>
198+
/// True when a peer DMA moving data FROM GPU <paramref name="from"/> TO GPU
199+
/// <paramref name="to"/> is known good. Matches the direction convention of
200+
/// <see cref="VerifyP2PRoundTrip"/> (which writes on <c>src</c> and reads
201+
/// back on <c>dst</c>) and of <see cref="CudaStorage.CopyDeviceFrom"/>.
202+
/// Callers must pass the direction the BYTES travel, not the direction of
203+
/// the context that happens to enqueue the copy.
204+
/// </summary>
189205
private bool CanAccessPeer(int from, int to) => _p2pEnabled[from * _worldSize + to];
190206

191207
private void EnsureStagingBuffer(long requiredBytes)
@@ -238,7 +254,14 @@ public void AllReduce(Tensor[] tensors)
238254
IntPtr srcPtr = DevicePtr(tensors[i]);
239255
IntPtr dstPtr = DevicePtr(tensors[0]);
240256

241-
if (CanAccessPeer(0, i) && _allocators[0].Kernels != null)
257+
// The bytes travel FROM GPU i TO GPU 0, so that is the direction to
258+
// vet. This used to ask CanAccessPeer(0, i) — the opposite link.
259+
// On a host where only one direction of a pair is corrupt (the
260+
// self-test above flags exactly that) the reduce then ran its
261+
// cuMemcpyPeerAsync over the known-bad direction while consulting
262+
// the healthy one, silently summing garbage into rank 0 on every
263+
// row-parallel layer.
264+
if (CanAccessPeer(i, 0) && _allocators[0].Kernels != null)
242265
{
243266
// P2P copy: GPU i → GPU 0 staging, then add.
244267
EnsureStagingBuffer(byteCount);
@@ -269,7 +292,9 @@ public void AllReduce(Tensor[] tensors)
269292
_allocators[i].Context.MakeCurrent();
270293
IntPtr dstPtr = DevicePtr(tensors[i]);
271294

272-
if (CanAccessPeer(i, 0))
295+
// Bytes travel FROM GPU 0 TO GPU i here (the mirror image of the
296+
// reduce above), so vet that direction.
297+
if (CanAccessPeer(0, i))
273298
{
274299
CudaDriverApi.cuMemcpyPeerAsync(
275300
dstPtr, _allocators[i].Context.Handle,

TensorSharp.Backends.Cuda/CudaQuantizedOps.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,11 @@ public static bool SupportsQuantizedType(int ggmlType)
417417
ggmlType == 14 || // Q6_K
418418
ggmlType == 16 || // IQ2_XXS
419419
ggmlType == 18 || // IQ3_XXS
420+
ggmlType == 20 || // IQ4_NL
420421
ggmlType == 21 || // IQ3_S
421422
ggmlType == 22 || // IQ2_S
422-
ggmlType == 23; // IQ4_XS
423+
ggmlType == 23 || // IQ4_XS
424+
ggmlType == 39; // MXFP4 (gpt-oss experts)
423425
}
424426

425427
/// <summary>

TensorSharp.Backends.Cuda/native/kernels/tensorsharp_kernels.cu

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
#define GGML_Q6_K 14
2222
#define GGML_IQ2_XXS 16
2323
#define GGML_IQ3_XXS 18
24+
#define GGML_IQ4_NL 20
2425
#define GGML_IQ3_S 21
2526
#define GGML_IQ2_S 22
2627
#define GGML_IQ4_XS 23
28+
#define GGML_MXFP4 39
2729
#define TS_QK8_1 32
2830
#define TS_Q80_F16_CHUNK 2048
2931
#define TS_Q80_BLOCK_BYTES 34
@@ -47,6 +49,20 @@ __device__ static const int8_t ts_kvalues_iq4nl[16] = {
4749
-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113
4850
};
4951

52+
// MXFP4 (OCP microscaling) E2M1 codebook, doubled -- the per-block E8M0 scale is
53+
// halved to compensate (ggml kvalues_mxfp4 + GGML_E8M0_TO_FP32_HALF).
54+
__device__ static const int8_t ts_kvalues_mxfp4[16] = {
55+
0, 1, 2, 3, 4, 6, 8, 12, 0, -1, -2, -3, -4, -6, -8, -12
56+
};
57+
58+
// E8M0 shared exponent -> 0.5 * 2^(e-127), bit-identical to ggml's
59+
// ggml_e8m0_to_fp32_half (denormal patterns for e < 2).
60+
__device__ __forceinline__ float ts_e8m0_to_fp32_half(uint8_t e)
61+
{
62+
uint32_t bits = (e < 2) ? (0x00200000u << e) : ((uint32_t)(e - 1) << 23);
63+
return __int_as_float((int)bits);
64+
}
65+
5066
struct ts_block_q8_1
5167
{
5268
half d;
@@ -114,6 +130,8 @@ __device__ __forceinline__ int qrow_bytes(int type, int cols)
114130
case GGML_IQ3_XXS: return (cols / 256) * 98;
115131
case GGML_IQ2_S: return (cols / 256) * 82;
116132
case GGML_IQ3_S: return (cols / 256) * 110;
133+
case GGML_IQ4_NL: return (cols / 32) * 18;
134+
case GGML_MXFP4: return (cols / 32) * 17;
117135
case GGML_IQ4_XS: return (cols / 256) * 136;
118136
default: return 0;
119137
}
@@ -422,6 +440,38 @@ __device__ __forceinline__ float qvalue_at(const uint8_t* row, int type, int col
422440
return (sign_byte & (1u << p)) ? -v : v;
423441
}
424442

443+
if (type == GGML_MXFP4)
444+
{
445+
// block_mxfp4: e (E8M0 byte), qs[16] = 17 bytes per 32 elements. Elements
446+
// 0..15 take the low nibble of qs[j] and 16..31 the high nibble, through the
447+
// E2M1 codebook scaled by the block's shared exponent
448+
// (ggml dequantize_row_mxfp4).
449+
const uint8_t* block = row + (col / 32) * 17;
450+
float d = ts_e8m0_to_fp32_half(block[0]);
451+
const uint8_t* qs = block + 1;
452+
int within = col & 31;
453+
int j = within & 15;
454+
uint8_t packed = qs[j];
455+
int nib = (within < 16) ? (packed & 0xF) : (packed >> 4);
456+
return d * (float)ts_kvalues_mxfp4[nib];
457+
}
458+
459+
if (type == GGML_IQ4_NL)
460+
{
461+
// block_iq4_nl: d (half), qs[16] = 18 bytes per 32 elements. One scale for
462+
// the whole block; elements 0..15 read the low nibble of qs[j] and 16..31
463+
// the high nibble, through the same non-linear codebook as IQ4_XS
464+
// (ggml dequantize_row_iq4_nl).
465+
const uint8_t* block = row + (col / 32) * 18;
466+
float d = __half2float(*reinterpret_cast<const half*>(block));
467+
const uint8_t* qs = block + 2;
468+
int within = col & 31;
469+
int j = within & 15;
470+
uint8_t packed = qs[j];
471+
int nib = (within < 16) ? (packed & 0xF) : (packed >> 4);
472+
return d * (float)ts_kvalues_iq4nl[nib];
473+
}
474+
425475
if (type == GGML_IQ4_XS)
426476
{
427477
// block_iq4_xs: d (half), scales_h (uint16), scales_l[4], qs[128] = 136 bytes.

TensorSharp.Backends.Cuda/native/ptx/tensorsharp_kernels.ptx

Lines changed: 18851 additions & 17514 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)