Skip to content

Commit 5453368

Browse files
[CUDA] New 4bit GEMM kernels for inference (#1949)
* New 4bit GEMM kernels for inference * Fix windows build * Fix some compile warnings * Fix trap for sm80 cubin running on sm86 * Fix lint * Narrow tests for CPU * XPU/MPS: add K%blocksize guard for gemv fallback in gemm_4bit
1 parent e55b5c0 commit 5453368

24 files changed

Lines changed: 2788 additions & 114 deletions

CMakeLists.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,29 @@ if(BUILD_CUDA)
234234

235235
list(APPEND SRC_FILES ${GPU_FILES})
236236

237+
# 4-bit GEMM SIMT and dispatch compile for all selected archs.
238+
# sm75/sm80+ MMA files only compile if those archs are selected.
239+
set(_cc_all ${COMPUTE_CAPABILITY} ${_LATEST_CAPABILITY})
240+
list(APPEND SRC_FILES csrc/gemm_4bit_simt.cu csrc/gemm_4bit.cu)
241+
242+
if(75 IN_LIST _cc_all)
243+
# Builds only on sm75
244+
list(APPEND SRC_FILES csrc/gemm_4bit_sm75.cu)
245+
add_compile_definitions(BNB_HAS_GEMM4BIT_SM75)
246+
endif()
247+
248+
set(_cc_sm80plus)
249+
foreach(_cc IN LISTS _cc_all)
250+
if(_cc GREATER_EQUAL 80)
251+
list(APPEND _cc_sm80plus ${_cc})
252+
endif()
253+
endforeach()
254+
if(_cc_sm80plus)
255+
# Builds only on sm80+
256+
list(APPEND SRC_FILES csrc/gemm_4bit_sm80.cu)
257+
add_compile_definitions(BNB_HAS_GEMM4BIT_SM80)
258+
endif()
259+
237260
string(APPEND BNB_OUTPUT_NAME "_cuda${CUDA_VERSION_SHORT}")
238261
add_compile_definitions(BUILD_CUDA)
239262
elseif(BUILD_HIP)

bitsandbytes/_ops.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,65 @@ def _(
220220
return out, absmax
221221

222222

223+
torch.library.define(
224+
"bitsandbytes::gemm_4bit",
225+
"(Tensor A, Tensor B, int[] shapeB, Tensor absmax, int blocksize, str quant_type, "
226+
"Tensor? bias=None, Tensor? absmax_8bit=None, Tensor? absmax_code=None, Tensor? absmax_offset=None) -> Tensor",
227+
)
228+
229+
230+
@register_fake("bitsandbytes::gemm_4bit")
231+
def _(
232+
A: torch.Tensor,
233+
B: torch.Tensor,
234+
shapeB: Sequence[int],
235+
absmax: torch.Tensor,
236+
blocksize: int,
237+
quant_type: str,
238+
bias: Optional[torch.Tensor] = None,
239+
absmax_8bit: Optional[torch.Tensor] = None,
240+
absmax_code: Optional[torch.Tensor] = None,
241+
absmax_offset: Optional[torch.Tensor] = None,
242+
) -> torch.Tensor:
243+
torch._check(len(shapeB) == 2, lambda: f"shapeB must be 2D [N, K], got {list(shapeB)}")
244+
torch._check(A.shape[-1] == shapeB[1], lambda: f"A inner dim ({A.shape[-1]}) must match shapeB ({shapeB[1]})")
245+
torch._check(
246+
A.dtype in (torch.float16, torch.bfloat16, torch.float32),
247+
lambda: f"A must be float16, bfloat16, or float32, got {A.dtype}",
248+
)
249+
torch._check(
250+
B.dtype in (torch.uint8, torch.bfloat16, torch.float16, torch.float32),
251+
lambda: f"B must be backed by storage of type uint8, bfloat16, float16, or float32, got {B.dtype}",
252+
)
253+
torch._check(blocksize in [32, 64, 128, 256, 512, 1024, 2048, 4096], lambda: f"invalid blocksize {blocksize}")
254+
torch._check(quant_type in ("nf4", "fp4"), lambda: f"quant_type must be 'nf4' or 'fp4', got {quant_type!r}")
255+
torch._check(absmax.dtype == torch.float32, lambda: f"absmax must be float32, got {absmax.dtype}")
256+
if absmax_8bit is not None:
257+
torch._check(absmax_8bit.ndim == 1, lambda: f"absmax_8bit must be 1D, got {absmax_8bit.ndim}D")
258+
torch._check(absmax_8bit.dtype == torch.uint8, lambda: f"absmax_8bit must be uint8, got {absmax_8bit.dtype}")
259+
torch._check(absmax_code is not None, lambda: "absmax_code required when absmax_8bit is provided")
260+
torch._check(absmax_code.ndim == 1, lambda: f"absmax_code must be 1D, got {absmax_code.ndim}D")
261+
torch._check(
262+
absmax_code.shape[0] == 256, lambda: f"absmax_code must have 256 entries, got {absmax_code.shape[0]}"
263+
)
264+
torch._check(
265+
absmax_code.dtype == torch.float32, lambda: f"absmax_code must be float32, got {absmax_code.dtype}"
266+
)
267+
torch._check(absmax_offset is not None, lambda: "absmax_offset required when absmax_8bit is provided")
268+
torch._check(
269+
absmax_offset.ndim == 0, lambda: f"absmax_offset must be a scalar (0-dim), got {absmax_offset.ndim}D"
270+
)
271+
torch._check(
272+
absmax_offset.dtype == torch.float32, lambda: f"absmax_offset must be float32, got {absmax_offset.dtype}"
273+
)
274+
if bias is not None:
275+
torch._check(bias.ndim == 1, lambda: f"bias must be 1D, got {bias.ndim}D")
276+
torch._check(bias.shape[0] == shapeB[0], lambda: f"bias length ({bias.shape[0]}) must match N ({shapeB[0]})")
277+
torch._check(bias.dtype == A.dtype, lambda: f"bias dtype ({bias.dtype}) must match A dtype ({A.dtype})")
278+
N = shapeB[0]
279+
return torch.empty((*A.shape[:-1], N), dtype=A.dtype, device=A.device)
280+
281+
223282
torch.library.define(
224283
"bitsandbytes::dequantize_blockwise",
225284
"(Tensor A, Tensor absmax, Tensor code, int blocksize, ScalarType dtype) -> Tensor",

bitsandbytes/autograd/_functions.py

Lines changed: 108 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,37 @@ def forward(ctx, A, B, out=None, bias=None, quant_state: Optional[F.QuantState]
315315
else:
316316
return torch.empty(A.shape[:-1] + B_shape[:1], dtype=A.dtype, device=A.device)
317317

318-
# 1. Dequantize
319-
# 2. MatmulnN
320-
output = torch.nn.functional.linear(A, F.dequantize_4bit(B, quant_state).to(A.dtype).t(), bias)
318+
# Normalize to canonical [(N*K+1)//2, 1]. Packed weights are always contiguous
319+
# in this orientation (B.t() callers get strides [1,1], still compatible).
320+
# quant_state.shape is the source of truth for N and K.
321+
B = B.view(-1, 1)
322+
323+
if not quant_state.nested:
324+
output = torch.ops.bitsandbytes.gemm_4bit.default(
325+
A,
326+
B,
327+
quant_state.shape,
328+
quant_state.absmax,
329+
quant_state.blocksize,
330+
quant_state.quant_type,
331+
bias=bias,
332+
)
333+
elif quant_state.state2.blocksize == 256:
334+
output = torch.ops.bitsandbytes.gemm_4bit.default(
335+
A,
336+
B,
337+
quant_state.shape,
338+
quant_state.state2.absmax,
339+
quant_state.blocksize,
340+
quant_state.quant_type,
341+
bias=bias,
342+
absmax_8bit=quant_state.absmax,
343+
absmax_code=quant_state.state2.code,
344+
absmax_offset=quant_state.offset,
345+
)
346+
else:
347+
raise NotImplementedError("nested quantization with state2.blocksize != 256 is not supported")
348+
321349
if out is not None:
322350
out.copy_(output)
323351
output = out
@@ -351,7 +379,9 @@ def backward(ctx, grad_output):
351379
# not supported by PyTorch. TODO: create work-around
352380
# if req_gradB: grad_B = torch.matmul(grad_output.t(), A)
353381
if req_gradA:
354-
grad_A = torch.matmul(grad_output, F.dequantize_4bit(B, ctx.state).to(grad_output.dtype).t())
382+
# B in ctx.tensors is already in canonical [(N*K+1)//2, 1] form (normalized in forward).
383+
# dequantize returns [N, K]; matmul(grad_output[M,N], [N,K]) = grad_A[M,K].
384+
grad_A = torch.matmul(grad_output, F.dequantize_4bit(B, ctx.state).to(grad_output.dtype))
355385

356386
return grad_A, grad_B, None, grad_bias, None
357387

@@ -381,26 +411,81 @@ def matmul_4bit(
381411
out: Optional[torch.Tensor] = None,
382412
bias: Optional[torch.Tensor] = None,
383413
):
384-
assert quant_state is not None
385-
if A.device.type == "cpu":
386-
if getattr(quant_state, "packing_format_for_cpu", False):
387-
out = F.gemv_4bit(A, B, out, state=quant_state)
388-
if bias is not None:
389-
out += bias
390-
return out
391-
else:
392-
return MatMul4Bit.apply(A, B, out, bias, quant_state)
393-
394-
if A.numel() == A.shape[-1] and A.requires_grad == False and A.device.type != "hpu":
395-
if A.shape[-1] % quant_state.blocksize != 0:
414+
if quant_state is None:
415+
raise ValueError("quant_state is required")
416+
if len(quant_state.shape) != 2:
417+
raise ValueError("matmul_4bit: quant_state.shape must be 2D [N, K]")
418+
419+
# packing_format_for_cpu uses a different memory layout optimized for AVX512BF16.
420+
# This flag is only set for inference (weight conversion happens at eval time).
421+
# The underlying kernel supports any M via tiled GEMM despite the gemv name.
422+
if A.device.type == "cpu" and getattr(quant_state, "packing_format_for_cpu", False):
423+
result = F.gemv_4bit(A, B, out=out, state=quant_state)
424+
if bias is not None:
425+
result += bias
426+
return result
427+
428+
# Normalize B to canonical [(N*K+1)//2, 1]. Packed weights are always contiguous
429+
# in this orientation (B.t() callers get strides [1,1], still compatible).
430+
# quant_state.shape is the source of truth for N and K.
431+
B = B.view(-1, 1)
432+
433+
K = A.shape[-1]
434+
435+
# Weight is in [K, N] orientation when A's inner dim matches shape[0] not shape[1].
436+
# Square weights (K==N) are ambiguous and treated as [N, K].
437+
if K == quant_state.shape[0] and K != quant_state.shape[1]:
438+
if not _is_compiling():
396439
warn(
397-
f"Some matrices hidden dimension is not a multiple of {quant_state.blocksize} and efficient inference kernels are not supported for these (slow). Matrix input size found: {A.shape}",
440+
f"matmul_4bit: weight was quantized from a [K, N] tensor (quant_state.shape={list(quant_state.shape)}). "
441+
"Re-quantize from the weight in [N, K] (out_features, in_features) orientation. "
442+
"This will be an error in a future version.",
443+
DeprecationWarning,
444+
stacklevel=2,
445+
)
446+
B_dq = F.dequantize_4bit(B, quant_state).to(A.dtype)
447+
result = torch.nn.functional.linear(A, B_dq.t(), bias)
448+
if out is not None:
449+
out.copy_(result)
450+
return out
451+
return result
452+
453+
needs_grad = torch.is_grad_enabled() and (A.requires_grad or (bias is not None and bias.requires_grad))
454+
if not needs_grad:
455+
A_numel = A.numel()
456+
if A_numel == 0:
457+
if out is not None:
458+
return out
459+
return torch.empty((*A.shape[:-1], quant_state.shape[0]), dtype=A.dtype, device=A.device)
460+
461+
if not quant_state.nested:
462+
result = torch.ops.bitsandbytes.gemm_4bit.default(
463+
A,
464+
B,
465+
quant_state.shape,
466+
quant_state.absmax,
467+
quant_state.blocksize,
468+
quant_state.quant_type,
469+
bias=bias,
470+
)
471+
elif quant_state.state2.blocksize == 256:
472+
result = torch.ops.bitsandbytes.gemm_4bit.default(
473+
A,
474+
B,
475+
quant_state.shape,
476+
quant_state.state2.absmax,
477+
quant_state.blocksize,
478+
quant_state.quant_type,
479+
bias=bias,
480+
absmax_8bit=quant_state.absmax,
481+
absmax_code=quant_state.state2.code,
482+
absmax_offset=quant_state.offset,
398483
)
399-
return MatMul4Bit.apply(A, B, out, bias, quant_state)
400484
else:
401-
out = F.gemv_4bit(A, B.t(), out, state=quant_state)
402-
if bias is not None:
403-
out += bias
485+
raise NotImplementedError("nested quantization with state2.blocksize != 256 is not supported")
486+
if out is not None:
487+
out.copy_(result)
404488
return out
405-
else:
406-
return MatMul4Bit.apply(A, B, out, bias, quant_state)
489+
return result
490+
491+
return MatMul4Bit.apply(A, B, out, bias, quant_state)

0 commit comments

Comments
 (0)