Skip to content

fix(cuda): chunk 4-bit quant/dequant launches above INT32_MAX elements (#1785) - #2071

Open
Anai-Guo wants to merge 1 commit into
bitsandbytes-foundation:mainfrom
Anai-Guo:fix/4bit-chunk-int32-launch
Open

fix(cuda): chunk 4-bit quant/dequant launches above INT32_MAX elements (#1785)#2071
Anai-Guo wants to merge 1 commit into
bitsandbytes-foundation:mainfrom
Anai-Guo:fix/4bit-chunk-int32-launch

Conversation

@Anai-Guo

@Anai-Guo Anai-Guo commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Problem

The CUDA blockwise 4-bit kernels take the element count as a 32-bit int
(ops.cu: void quantizeBlockwise(..., const int n)), and the Python side passes it
through a ct.c_int32 argtype. A tensor with numel() >= 2**31 therefore wraps the
count negative, num_blocks = n / blocksize goes negative, and the launch fails:

Error invalid configuration argument at line 62 in file .../csrc/ops.cu

This is the 4-bit half of #1785 ("Support quantizing tensors when numel() > INT_MAX",
listed there under high priority), and was reported again in #2067 for
google/gemma-4-E4B-it's embedding under axolotl's NF4 round-trip simulation. It also
hits fused-MoE expert weights held as one parameter, e.g. [128, 4096, 4096] == 2**31.

Reproduced on an RTX 4090 (bitsandbytes 0.49.2, torch 2.5.1+cu121), one process per size:

n before
2**31 - 65536 OK
2**31 invalid configuration argument
2**31 + 65536 invalid configuration argument

Change

quantize_4bit / _dequantize_4bit_impl in the CUDA backend now issue the work as
several blocksize-aligned launches instead of one. Chunks start on a block boundary,
so per-block absmax entries and the two-elements-per-byte 4-bit packing stay aligned;
each chunk's element count fits in int32, so in-kernel indexing stays in range too.

Tensors within the limit yield a single (0, n) chunk with byte-identical kernel
arguments, i.e. the previous behaviour is untouched on the common path. No C ABI change,
so existing ctypes callers are unaffected.

Scoped to the 4-bit ops (the high-priority pair in #1785). 8-bit blockwise quantization,
4-bit GEMV and the LLM.int8() ops still have the same int32 limit; kernel-level int64
indexing remains the longer-term fix.

Prior art: #2049 by @alex-ht took the same chunking approach at the functional.py
level; it was closed by its author. This one sits in the CUDA backend, where the int32
ABI actually is, so non-CUDA backends are left alone.

Verification (RTX 4090, 24 GB)

1. The reported failure is fixed, and the data is correct — not just "it doesn't crash".
Quantize + dequantize a real 2**31-element fp16 tensor (nf4, blocksize 64) and compare
against the input. The chunk seam falls at element 2147483584:

quantize   n=2**31 OK  q=(1073741824, 1) absmax=(33554432,)
dequantize n=2**31 OK  D=(2147483648,) dtype=torch.float16
  head     : rel_l2_err=0.09210  corr=0.995757
  pre-seam : rel_l2_err=0.09195  corr=0.995772
  post-seam: rel_l2_err=0.09213  corr=0.995620
  tail     : rel_l2_err=0.09195  corr=0.995772
  WHOLE TENSOR rel_l2_err=0.09198

The error is flat across the seam and matches NF4 quality on N(0,1) — the second chunk
is real data, not garbage written at a wrong offset.

2. Bit-exact against the single-launch path. With the per-launch limit lowered so the
chunked path runs on small tensors, q, absmax and the dequantized output are all
torch.equal to the single-launch results across 3 dtypes x {fp4, nf4} x 8 blocksizes
(48 cases), with a deliberately unaligned limit and a partial trailing block.

3. The new test is not vacuous. It runs 8 launches per case, and mutating the packed
offset (_off // 2 -> _off // 4) fails all 48 cases.

4. Lint. ruff==0.14.3 (the pinned .pre-commit-config.yaml rev) check and
format --check clean on both files.

🤖 Generated with Claude Code

bitsandbytes-foundation#1785)

The CUDA blockwise 4-bit kernels take the element count as a 32-bit int, so a
single launch cannot describe a tensor with more than INT32_MAX elements: the
count wraps negative, the derived grid dimension goes negative and the launch
fails with cudaErrorInvalidValue ("invalid configuration argument" at ops.cu).

Issue the work as several blocksize-aligned launches instead. Chunks start on a
block boundary, so per-block absmax entries and the two-elements-per-byte 4-bit
packing stay aligned; tensors within the limit still take a single launch with
byte-identical arguments.

Signed-off-by: Tai An <antai12232931@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant