From 5e4a22e3e6a6248933971ddf742dca25bf0953a8 Mon Sep 17 00:00:00 2001 From: James Burton Date: Sun, 9 Aug 2026 18:45:36 +0100 Subject: [PATCH] fix(cuda): widen byte-size products on the weight-upload path (#432) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while auditing the other backends for the #429 bug class. The native kernels are already correct — every weight-row base in native/kernels/ uses `(size_t)row * blocks_per_row * BYTES`, and the widening was confirmed to survive into the checked-in PTX. These three are managed-side. CudaKernels.cs LaunchDequantToF16: `(nuint)(totalElements * 2)` is int*int and wraps negative above 1,073,741,823 elements (a 2 GiB F16 tensor), after which `(nuint)` sign-extends it to a ~16 EiB byte count for cuMemcpyDtoD_v2. The Llama 3.1 405B LM head (128256 x 16384 = 2,101,346,304) wraps; the 70B head (1,050,673,152) clears it by only 2.2%. CudaWeights.cs UploadAndDequant: `outputDim * inputDim` wraps at 2^31 elements. Made `checked` rather than widened, deliberately — every byte size derived from it already casts to long, but the count is passed as `int` to LaunchConvertF32ToF16 / LaunchDequantToF16, so a long value could not be honoured without changing those signatures. Supporting >2^31-element tensors is a larger piece of work; this turns silent corruption into a clear failure. CudaTransformerModel.cs: `(seqLen - 1) * hiddenSize * h` is an activation offset and self-limiting, so consistency only — sibling lines in the same file already cast (long), and the inconsistency invites the wrong conclusion about which form is correct. Not verified on hardware: no NVIDIA GPU available here, and the overflowing scale is out of reach of the 12/16 GB cards I can borrow. The changes are type-level and the arithmetic is in the issue. --- src/DotLLM.Cuda/CudaKernels.cs | 2 +- src/DotLLM.Cuda/CudaTransformerModel.cs | 2 +- src/DotLLM.Cuda/CudaWeights.cs | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/DotLLM.Cuda/CudaKernels.cs b/src/DotLLM.Cuda/CudaKernels.cs index ec4d059d..b2ffd86d 100644 --- a/src/DotLLM.Cuda/CudaKernels.cs +++ b/src/DotLLM.Cuda/CudaKernels.cs @@ -604,7 +604,7 @@ public void LaunchDequantToF16(nint src, QuantizationType srcDtype, { case QuantizationType.F16: // Already FP16, just copy - CudaDriverApi.cuMemcpyDtoD_v2(dst, src, (nuint)(totalElements * 2)).ThrowOnError(); + CudaDriverApi.cuMemcpyDtoD_v2(dst, src, (nuint)((long)totalElements * 2)).ThrowOnError(); return; case QuantizationType.F32: diff --git a/src/DotLLM.Cuda/CudaTransformerModel.cs b/src/DotLLM.Cuda/CudaTransformerModel.cs index 8591984c..eb80b271 100644 --- a/src/DotLLM.Cuda/CudaTransformerModel.cs +++ b/src/DotLLM.Cuda/CudaTransformerModel.cs @@ -287,7 +287,7 @@ public ITensor Forward(ReadOnlySpan tokenIds, ReadOnlySpan positions, } // 5. Final RmsNorm (last token only) - nint lastHidden = _state.HiddenState + (nint)((seqLen - 1) * hiddenSize * h); + nint lastHidden = _state.HiddenState + (nint)((long)(seqLen - 1) * hiddenSize * h); _kernels.LaunchRmsNorm(lastHidden, _weights.OutputNormWeight, _state.NormOutput, hiddenSize, eps, 1, s); diff --git a/src/DotLLM.Cuda/CudaWeights.cs b/src/DotLLM.Cuda/CudaWeights.cs index 84f9a654..62a0f081 100644 --- a/src/DotLLM.Cuda/CudaWeights.cs +++ b/src/DotLLM.Cuda/CudaWeights.cs @@ -242,7 +242,11 @@ private static nint UploadAndDequant(nint hostPtr, QuantizationType qt, int outputDim, int inputDim, List allocs, CudaKernels kernels, nint stream) { - int totalElements = outputDim * inputDim; + // `checked` rather than widened: every byte size below already casts to long, so this + // element count is the only narrow product — but it is also passed as `int` to + // LaunchConvertF32ToF16 / LaunchDequantToF16, so a long count could not be honoured + // without changing those signatures. Fail loudly instead of wrapping negative (#429). + int totalElements = checked(outputDim * inputDim); if (qt == QuantizationType.F16) {