Repository navigation
fix(exl3): bound tile scratch and keep sign vectors fp16 on load - #3546
Merged
Merged
Conversation
…zed launches EXL3 allocates its tile scratch as `batch x 256 x (65536 >> K)` int16, which is 2 GiB at the hardcoded batch of 256. The buffer is per CUDA stream and the quantizer runs one stream per worker thread, so a wide run can hold several GiB of pure scratch per device. On a 48 GiB card that is enough to make a 65B quantization fail to start, and it made a 2-layer fixture require ~6 GiB before doing any work. GPTQMODEL_EXL3_TILE_BATCH now sets the slot count (default 256, unchanged). When a tile set exceeds the slots the launch is split; tiles carry their own trellis and write all 256 of their entries, so batching them is exactly equivalent to a single launch. Verified bit-identical output on a 2-layer Ripple fixture at 2.5 bpw: all 81 saved tensors match between tile batches 8 and 32 (`torch.equal`, 0 of 81 mismatched), and the reloaded model forwards with fp16 logits. Tests cover the default, the clamping of out-of-range values, and the launch split (5 tiles at 2 slots -> 2+2+1).
`LinearEXL3` requires `suh`/`svh` to be `torch.half` because the Hadamard
kernels are typed for fp16. They are stored that way in the checkpoint, but
`nn.Module._apply` casts every floating-point buffer to the model dtype, so
loading an EXL3 checkpoint with `dtype=bfloat16` produced bf16 sign vectors
and the first forward died with:
AssertionError: suh is wrong datatype
The quantizer happily writes bf16 checkpoints, so the defect only surfaced on
reload, which is exactly when a saved artifact is supposed to be trustworthy.
Reproduced on a 2-layer Ripple fixture: the same checkpoint loads at fp16 and
fails at bf16.
Restore the kernel dtypes after every cast, in `_apply` and again in
`_ensure_inner`/`post_init` so the invariant holds however the module is
materialized. Packed `su`/`sv` are int16 and are untouched.
Verified on the fixture checkpoint: bf16 reload now forwards and generates,
repeated forwards are bit-identical, and fp16-vs-bf16 logits differ by 0.018
(bf16 rounding on the non-quantized tensors).
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.
Two independent EXL3 defects found while producing a 2.5 bpw checkpoint.
1.
perf(exl3)— tile scratch is 2 GiB per streamEXL3 allocates its tile scratch in
_allocate_temp_buffersastemp_edgesis 2 GiB at K=2, and the buffer is keyed per CUDA stream while the quantizer runs one stream per worker thread (get_quant_stream). A wide run therefore holds several GiB of pure scratch per device.Two concrete consequences observed:
The batch of 256 is chosen for launch occupancy (a 56-tile grid fills less than a quarter of the machine), so it should not simply be lowered for everyone.
Change
GPTQMODEL_EXL3_TILE_BATCHsets the slot count. Default is 256 — behaviour is unchanged unless the variable is set. Values are clamped to[1, 256]and malformed values fall back to the default._quantize_tiles_with_workspacesplits the launch. Tiles carry their own trellis and each writes all 256 of its entries, so there is no cross-tile state and batching is exactly equivalent to one launch.Verification
Bit-identical output on the Ripple fixture at 2.5 bpw EXL3:
torch.equal, shapes and dtypes identical)bits=2.5, method=exl3, format=exl3, codebook=mcg(4, 64, 256)float16Tests (
tests/test_exl3_workspace.py, 4 passed / 1 skipped, ruff clean):8/1/0/-5/4096/junkclamp or fall back as documented;2 + 2 + 1, and a set that fits is not split;2.
fix(exl3)— bf16 checkpoints are unloadableLinearEXL3asserts the unpacked sign vectors aretorch.half:The Hadamard kernels are typed for fp16, and the checkpoint stores them that way. But
nn.Module._applycasts every floating-point buffer to the model dtype, so loading withdtype=bfloat16produces bf16 sign vectors and the first forward dies withAssertionError: suh is wrong datatype.The quantizer writes bf16 checkpoints without complaint, so the defect only appears on reload — when the saved artifact is supposed to be trustworthy. Reproduced on a 2-layer Ripple fixture:
float16bfloat16(the checkpoint default)AssertionError: suh is wrong datatypeChange
ExllamaV3Linearrestores the kernel dtypes after every cast, in_applyand again in_ensure_inner/post_init, so the invariant holds however the module is materialized. Packedsu/svare int16 and are untouched by the cast.Verification
On the fixture checkpoint:
0.018(bf16 rounding on the non-quantized tensors only).Tests:
tests/test_exl3_buffer_dtype.py(4 passed, hardware-free) covers bf16 and fp32 casts, int16 preservation, and sign-value preservation.tests/test_exllamav3_meta_import.py,test_exl3_rng_isolation.py,test_exl3_workspace.pystill pass (7 passed / 4 skipped). Ruff clean.Notes
Both changes are prerequisites for running EXL3 on a 65B with a per-device memory budget. Neither alters the quantizer's numerics: the tile batching is bit-identical, and the dtype fix only affects buffer dtypes on load.