Repository navigation
perf(quantization): stream int8 convrot weights as int8 under partial loading - #9845
Open
Pfannkuchensack wants to merge 3 commits into
Open
Pfannkuchensack wants to merge 3 commits into
Pfannkuchensack wants to merge 3 commits into
Conversation
…dening A CPU-resident Int8ConvrotLinear streamed its weight with one .to(device=, dtype=), which converts on the host and transfers the widened tensor; partial loads of int8 convrot models now copy int8 and cast on the device, bit-identical output.
This branch has not been deployed
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.
Summary
Under partial loading, an
Int8ConvrotLinearwhose int8 buffers stayed in RAM streamed its weight with a singleself.weight.to(device=device, dtype=dtype). For a CPU→GPU copy that changes dtype, PyTorch converts on the CPU first and then transfers the widened tensor: a host-side int8→bf16 pass plus twice the PCIe bytes, for every streamed layer on every forward.The weight is now moved first and widened on the device (
.to(device=device).to(dtype=dtype)). Output is bit-identical and the peak transient is unchanged: the chain frees the int8 copy before the scale multiply allocates, sodequant_transient_bytesstays as it is. The fully resident path is unaffected, since.to(device)is a no-op there.Measured on NVIDIA (RTX 4090, CUDA) and AMD (RX 9060 XT, ROCm), with pixel-identical images on both. Partial-load generations got 57–70% faster on the 4090 and 6–18% faster on the RX 9060 XT; that card's gain is limited by its narrow PCIe link (see QA Instructions).
This covers every int8_tensorwise(+convrot) model, because they all share
Int8ConvrotLinear: Krea-2, FLUX.1/FLUX.2, Z-Image, Qwen-Image, Anima, Ideogram 4, LTX-2, MiniMax H3, PiD and the int8 text encoders. No other per-forward streaming path had the pattern. nvfp4, scaled fp8, GGUF, SDNQ, bnb and the autocast wrappers already move to the device before casting or dequantizing.Related Issues / Discussions
Closes #9773
QA Instructions
NVIDIA (CUDA)
Measured on an RTX 4090 with torch 2.13.0+cu130 on Windows. The A/B runs were interleaved in one server process (warm-up, then B A A B B A A B,
use_cache=false), with the VRAM cache capped throughINVOKEAI_MAX_CACHE_VRAM_GBto force partial loading. Both arms produced pixel-identical images.Per streamed layer (microbenchmark, unpinned host memory, bf16), the decode is 4.8–6.5× faster across 3072×3072 to 10240×3840. Results are bit-identical and the peak allocation stays at 4.00 B/element.
Tests:
pytest -m "slow or not slow" tests/backend/quantization/test_int8_convrot.py tests/backend/quantization/test_int8_convrot_lora_sidecar.py tests/backend/quantization/test_quantized_payloads.py: 39 passed, 1 skipped.test_a_layer_left_in_ram_moves_its_int8_codes_before_widening_them(runs on CPU via the meta device): fails against the previous one-step.to(device=, dtype=).test_a_layer_left_in_ram_streams_its_int8_codes_within_the_transient_estimate: fails if the streamed int8 copy is kept alive through the decode (5.2 MB vs. 4.2 MB on its fixture). It skips undercudaMallocAsync, which does not track requested bytes.ruff checkandruff format --checkpass on the changed files.AMD (ROCm)
Measured on an RX 9060 XT (16 GB, torch 2.13.0+rocm10.0.0, Windows) with the same method and the same tests (39 passed, 1 unrelated skip):
On this card each streamed layer is 2.1× faster, against ~5× on the 4090. The card sits on an M.2 adapter (PCIe 4.0 ×4) and moves only 2.2 GiB/s from unpinned memory, compared with 12.1 GiB/s on the 4090. So the transfer dominates there, and halving its bytes is the whole gain. On the 4090, the host-side int8→bf16 cast was the larger cost: 12.6 ms for a 36 MiB layer, against 6.2 ms to copy the bf16 result. A card attached at full width should land between the two.
Review
No blocking findings. Changes made in response to review:
Remaining limitation: with the native caching allocator, the extra int8-sized blocks raised peak reserved memory by about one int8 copy of the largest layer: 36 MiB over a 30-layer mixed-shape loop, with peak allocated unchanged. With
cudaMallocAsyncthis does not occur.Checklist
What's Newcopy (if doing a release after this PR)