Cache device-side constants in dequant kernels - #472
Conversation
The dequantize_blocks_* functions build their bit-shift vectors inline with torch.tensor([...], device=...) on every call. Each one is a pageable host-to-device copy plus an implicit sync, and dequantize runs once per quantized tensor per forward pass, so the cost is paid thousands of times per sampling step. Measured cost of a single torch.tensor([0, 4], device=gpu): RTX 4050 Laptop (internal PCIe) : 307.6 us -> 26.0 us cached RX 9060 XT (USB4 eGPU, 2.75GB/s): 178.6 us -> 1.8 us cached Constants are keyed on (values, device, dtype) over the fixed set of literals used in this file, so the cache holds a handful of tiny tensors per device. KVALUES gets the same treatment instead of being re-copied to the device on every IQ4 call. Output is bit-identical across all 13 dequantizable qtypes.
|
Okay, it looks like you did the optimization that TorchDynamo/Torch.compile was doing. PR + Compile: 75s m8rr@1c113ea For reference, I am using Oculink PCIe 4x4, so that might be why it's so effective. However, in text generation, there is a huge performance difference with torch.compile: How about expanding to low-beat quants as well? |
Problem
Every
dequantize_blocks_*function that needs a bit-shift vector builds it inline:torch.tensor(..., device=gpu)is a pageable host-to-device copy plus an implicit sync. Sincedequantizeruns once per quantized tensor per forward pass, this is paid thousands of times per sampling step.KVALUES.to(qs.device)in the IQ4 paths has the same issue.I found this while profiling a stalled LTX-Video run —
py-spyshowed 100% of samples insidedequantize_blocks_Q4_K/Q5_K, and it turned out to be the constant allocation rather than the arithmetic.Measured cost of a single
torch.tensor([0, 4], device=gpu):It is not eGPU-specific — the internal-PCIe laptop GPU was actually worse in absolute terms. eGPU just made it impossible to ignore.
Change
Cache the constants keyed on
(values, device, dtype). The literals used in this file are a fixed small set, so the cache holds a handful of tiny tensors per device — it does not grow with model size or step count.KVALUESgets a cached device copy instead of being re-uploaded on every IQ4 call.No behavioural change otherwise; the tensors were already immutable and only ever used as shift operands.
Verification
Compared every dequant function against the current
mainimplementation on identical random input, then benchmarked both. 4096 blocks, bf16 target, 60 iterations after 15 warmup:Q8_0 is a useful control — it is the only qtype with no shift constant, and it moves the least.
End-to-end on a real workflow (LTX-Video 22B Q4_K_S, 8-step sampling + 3-step upscale + VAE decode, same seed and settings,
--reserve-vramtuned so there is zero weight offload in both runs):1.41x, output visually identical.
Repro script
bench_dequant_constants.py
Notes
This is orthogonal to #336 — that replaces the dequant kernels with Triton implementations, this just stops re-allocating constants inside the existing ones. If #336 lands, these call sites disappear anyway, but this is a small independent win in the meantime.
Tested on ROCm (gfx1200, torch 2.10) and CUDA (RTX 4050, torch 2.x). Happy to adjust naming or drop the inline comment block if you prefer it leaner.