Skip to content

[cuda backend] reduce memory consumption on gemma4_31b by running embedding in int8#20351

Merged
Gasoonjia merged 7 commits into
mainfrom
g4-gguf-export-mem
Jun 22, 2026
Merged

[cuda backend] reduce memory consumption on gemma4_31b by running embedding in int8#20351
Gasoonjia merged 7 commits into
mainfrom
g4-gguf-export-mem

Conversation

@Gasoonjia

@Gasoonjia Gasoonjia commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Decode the tied GGUF token_embd/lm_head weight to a gatherable int8 IntxUnpackedToInt8Tensor for the embedding instead of dequantizing the whole ~1.4B-element weight to bf16 to reduce the memory consumption during exporation
Halves the embedding's host + GPU-constant footprint (2 -> 1 B/elem) and reduce about 1.5 gb vram memory.

The decode is lossless for Q6_K; for Q4_K the int8 embedding matches the precision of the tied lm_head and the other q4_k linears (same bf16 zero-point reconstruction), so it adds no error beyond the model's existing 4-bit level. Also create test case to verify it.

lm_head keeps its packed format (Q6_K -> CudaDp4aPlanarInt6Tensor from the same int8 decode; Q4_K -> CudaCoalescedInt4Tensor via _resolve_tied_lm_head). MLX and non-Q4_K/Q6_K paths unchanged.

Memory consumption: 28.3 GB with context length == 128k + turboquant

TurboQuantKVCache.update wrote the compressed cache via slice index-assignment (self.k_packed[:, :, input_pos] = ...), which lowers to index_put_ and breaks CUDA-graph capture of the decode step. Switch the 4 cache writes to index_copy_(2, input_pos, ...): a static scatter along the position dim, matching the model's flat global KV cache (Gemma4KVCache.update). Nibble packing is on the last dim (D/2); index_copy_ along dim 2 (positions) leaves the packed layout untouched. MLX overrides update(); qwen shares this path (test_turboquant 5/5).
Builds on the kv_len decode clamp. After that clamp, prefill still scanned ALL KV blocks in [0, chunk_end] for every query row, paying the full causal upper-triangle. Add a per-tile causal upper bound to tq4_sdpa's loop, gated behind a new mask_is_causal op arg (threaded through register_fake, both autotune wrappers, launcher, and body): loop_end = min(kv_len, (kv_len - Lq) + max(seq_pos) + 1). seq_pos is the query-row index; the (kv_len - Lq) offset converts it to an absolute KV position, so it is correct for chunked prefill. gemma's full-attention (global) layers pass mask_is_causal=True; sliding layers are bf16 (untouched) and qwen does not pass the flag (defaults False). Decode-safe: at Lq=1 loop_end == kv_len, so decode is byte-identical. Skipped blocks are fully masked, so prefill is bit-identical to no-skip, just faster.
…ontext) decode/prefill)

The fused TQ4 attention kernel iterated the full pre-allocated KV buffer (max_seq_len) every decode/prefill step, masking empty/future blocks only after computing them, making the 10 global layers O(max_seq_len) regardless of actual context. Add an optional kv_len GPU int32 scalar to triton::tq4_sdpa that bounds the inner loop to the valid/filled length. gemma's call site passes kv_len = input_pos[0] + T (decode: pos+1; prefill chunk: chunk_end). kv_len is read on-device via tl.load (no .item(), no host sync) so the bound updates across CUDA-graph replays. When kv_len is None, HAS_KV_LEN is False and the loop runs over full Lk, so the shared qwen path is byte-identical.
…only')

TQ4 KV cache long-context is now supported on the CUDA backend, not just MLX. Update the README section: retitle to CUDA + MLX, add a CUDA 128k example, and state that long context requires BOTH --turboquant and a larger --max-seq-len (raising --max-seq-len alone keeps a bf16 KV cache that does not fit at 128k). Note the ~27 GiB runtime peak fits a 32 GB card.
…t export memory

Decode the tied GGUF token_embd/lm_head weight to a gatherable int8
IntxUnpackedToInt8Tensor for the embedding instead of dequantizing the whole
~1.4B-element weight to bf16. Halves the embedding's host + GPU-constant
footprint (2 -> 1 B/elem). The decode is lossless for Q6_K; for Q4_K the int8
embedding matches the precision of the tied lm_head and the other q4_k linears
(same bf16 zero-point reconstruction), so it adds no error beyond the model's
existing 4-bit level. lm_head keeps its packed format (Q6_K ->
CudaDp4aPlanarInt6Tensor from the same int8 decode; Q4_K -> CudaCoalescedInt4Tensor
via _resolve_tied_lm_head). MLX and non-Q4_K/Q6_K paths unchanged.
@pytorch-bot

pytorch-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/20351

Note: Links to docs will display an error until the docs builds have been completed.

❌ 3 New Failures, 1 Cancelled Job, 4 Unrelated Failures

As of commit e322831 with merge base 63b4c4d (image):

NEW FAILURES - The following jobs have failed:

CANCELLED JOB - The following job was cancelled. Please retry:

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

BROKEN TRUNK - The following jobs failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 17, 2026
def test_load_converts_weights(self):
"""GGUF -> CUDA: Q4_K -> CudaCoalescedInt4Tensor, Q6_K -> CudaDp4aPlanarInt6Tensor,
embedding bf16."""
embedding int8 (gatherable)."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is cuda using int8 embedding + lm_head tied now?

Before it used int8 embedding + int4 lm_head? How does that change perf?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no it is still int4 lm_head. This PR only switches the GGUF embedding from bf16 to int8 (it was previously dequantized to bf16); lm_head is unchanged.

@mergennachin mergennachin changed the title [cuda backend] reduce memory consumption on gemma4_31b [cuda backend] reduce memory consumption on gemma4_31b by running embedding in int8 Jun 18, 2026
Base automatically changed from g4-128k-context to main June 18, 2026 21:46
@Gasoonjia Gasoonjia requested a review from larryliu0820 as a code owner June 18, 2026 21:46
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Gasoonjia Gasoonjia merged commit 39c0df6 into main Jun 22, 2026
215 of 223 checks passed
@Gasoonjia Gasoonjia deleted the g4-gguf-export-mem branch June 22, 2026 22:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants