Warn once when Linear4bit computes in float32 on GPU - #2077
Open
thangdangjp wants to merge 1 commit into
Open
thangdangjp wants to merge 1 commit into
thangdangjp wants to merge 1 commit into
Conversation
`Linear4bit` has warnings for a float32 compute_dtype, but they live in `set_compute_type()`, which `forward()` only calls when `compute_type_is_set` is False -- i.e. when no compute_dtype was passed to the constructor. The dominant integration always passes one: `transformers.BitsAndBytesConfig` defaults `bnb_4bit_compute_dtype` to `torch.float32`, so a plain `BitsAndBytesConfig(load_in_4bit=True)` pins every layer to float32 compute and the existing warnings are unreachable. Users get the slow path with no runtime signal at all. That path is slow because float32 has no MMA 4-bit GEMM kernel, as the CUDA dispatch heuristic itself notes -- float32 only takes the custom kernel for M < 8 and otherwise falls back to unfused dequantize + F.linear. Measured on an A100 80GB (bnb 0.50, torch 2.13+cu130) with Qwen3-4B projection shapes, float32 vs bfloat16 compute_dtype: shape M=1 M=8 M=512 M=2048 q_proj (K=2560, N=4096) 0.68x 1.45x 5.64x 8.25x o_proj (K=4096, N=2560) 0.68x 0.84x 6.24x 7.73x gate_proj (K=2560, N=9728) 0.79x 2.97x 6.58x 9.30x down_proj (K=9728, N=2560) 0.81x 1.84x 7.91x 9.90x (>1 means float32 is slower). So single-token decoding is unaffected, but prefill and training run several times slower than they need to. Log a hint once per process per device type when a Linear4bit actually computes in float32 on a CUDA device. The check sits in `forward()` so it covers every way a module can reach that state, including the inferred one where `set_compute_type()` adopts float32 from float32 inputs. Two guards keep it cheap and safe: a `_fp32_compute_warned` latch reduces the steady state to one attribute load, and `torch.compiler.is_compiling()` keeps the branch out of Dynamo graphs so `fullgraph=True` compilation is unaffected. Also fix a stale comment claiming float32 is adopted "for speed", and a "compoute" typo. Author: Thang Dang - Fujitsu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Linear4bitalready has warnings for a float32compute_dtype, but they are unreachable in the case that matters most.They live in
set_compute_type(), whichforward()only calls whencompute_type_is_setisFalse— that is, only when nocompute_dtypewas passed to the constructor:But the dominant integration always passes one.
transformers.BitsAndBytesConfigdefaultsbnb_4bit_compute_dtypetotorch.float32, andintegrations/bitsandbytes.pyforwards it toLinear4bit(...). So a plainBitsAndBytesConfig(load_in_4bit=True)setscompute_type_is_set = Truewithcompute_dtype=torch.float32,set_compute_type()never runs, and the user lands on the slow path with no runtime signal at all — even when the model itself was loaded in bfloat16.Additionally, the branch that does run silently picks float32 itself when it sees float32 inputs, under a comment claiming that is done "for speed and stability". The float16 branch warns about exactly this situation; the float32 branch says nothing.
Why float32 compute is slow
Because there is no MMA 4-bit GEMM kernel for float32 — as the CUDA dispatch heuristic in
backends/cuda/ops.pynotes itself:So float32 takes the custom kernel only for
M < 8and otherwise falls back to unfused dequantize +F.linear.Measured on an A100 80GB PCIe (bitsandbytes 0.50, torch 2.13.0+cu130),
bnb.nn.Linear4bitwith nf4 + double quant at Qwen3-4B projection shapes,compute_dtype=float32vsbfloat16, float32 inputs in both cases. Values are float32 time / bfloat16 time, so >1 means float32 is slower:q_proj(K=2560, N=4096)k_proj(K=2560, N=1024)o_proj(K=4096, N=2560)gate_proj(K=2560, N=9728)down_proj(K=9728, N=2560)Single-token decoding is unaffected (slightly faster, in fact), which is part of why this is easy to miss. Prefill and training, however, run several times slower than they need to.
This is also easy to hit accidentally in a QLoRA setup: peft's
prepare_model_for_kbit_training()upcasts norms and embeddings to float32, soLinear4bitlayers receive float32 activations, and ifbnb_4bit_compute_dtypewas left at its default the whole model computes in float32.Change
Log a hint once per process per device type when a
Linear4bitactually computes in float32 on a CUDA device.The check sits in
forward()rather than inset_compute_type()or__init__(), because that is the only place that sees both the resolvedcompute_dtypeand the real device, and it therefore covers every route into this state — the explicitly-configured one above, the inferred one whereset_compute_type()adopts float32 from float32 inputs, and pre-quantized checkpoints loaded viadevice_map(which bypassParams4bit._quantize).Two guards keep that safe:
_fp32_compute_warned, a class-level default shadowed per instance, so the steady-state cost on the hot path is one attribute load and a negation. Deduplication across modules is handled byfunctools.cacheon the log helper, which matters because a 4-bit model has hundreds of these layers (cf.gemm_4bit: blocksize-alignment warning emitted on every call, and now also during training (new in 0.50.0) #2027, where a per-call warning was itself the bug).torch.compiler.is_compiling(), so the branch is constant-folded away under Dynamo and never causes a graph break.The message names both
bnb_4bit_compute_dtypeandLinear4bit(compute_dtype=...), notes that float32 is the Transformers default, allows for users who genuinely want float32, and says how to silence it.Also fixes the stale "for speed and stability" comment and a
compoutetypo.Testing
tests/test_modules.py::test_4bit_linear_fp32_compute_dtype_warningcovers three cases across all available devices: explicitly-configured float32 warns exactly once over 4 layers × 3 forwards; float32 inferred byset_compute_type()also warns once; bfloat16 stays silent. It asserts the warning appears only oncuda.Run on an A100 80GB, CUDA 13.0, torch 2.13.0+cu130, Python 3.12:
tests/test_modules.py— 177 passedtests/test_linear4bit.py -k "not torch_compile"— 357 passedtests/test_linear4bit.py::test_linear4bit_torch_compile, all 32fullgraph=True+fp32+cudavariants — 32 passed, confirming no graph breakpre-commit run --all-files— all 10 hooks passI also verified the deduplication directly outside pytest: 200
Linear4bitlayers × 5 forwards produces exactly 1 log record.Submitted by Thang Dang, Fujitsu Research of America