1717 linear and embedding. ``embed_tokens`` and ``lm_head`` stay tied -- they share
1818 the one quantized tensor.
1919* **CUDA**: Q4_K -> ``Int4Tensor``, Q6_K -> ``CudaDp4aPlanarInt6Tensor`` (a genuine
20- 6-bit packed weight, lossless, symmetric); ``lm_head`` keeps the quantized
21- tensor but the token embedding is dequantized to bf16 (the packed tensors can't
22- gather), so they are untied.
20+ 6-bit packed weight, lossless, symmetric). ``embed_tokens`` and ``lm_head`` are
21+ untied: ``lm_head`` keeps a packed (int6/int4) matmul weight, while the token
22+ embedding becomes a gatherable ``IntxUnpackedToInt8Tensor`` (int8) -- the truly
23+ packed int4/int6 tensors can't gather. For the Q6_K tied weight the decode is
24+ done once and shared between the two, avoiding a whole-tensor bf16 dequant and
25+ a second decode (see ``_untie_embed_lm_head``).
2326
2427Usage:
2528 model, config = load_gguf_model("model.gguf", backend="cuda")
@@ -116,6 +119,55 @@ def _resolve_tied_lm_head(model, lm_head_weight, packers):
116119 )
117120
118121
122+ def _untie_embed_lm_head (model , gtensor , weight , backend ):
123+ """Untie the GGUF token-embed / lm_head weight, returning ``(embed, lm_head)``.
124+
125+ GGUF ties ``embed_tokens`` and ``lm_head`` to one quantized weight. The
126+ returned ``lm_head`` is packed into ``model.lm_head`` after the streaming loop
127+ (``_resolve_tied_lm_head``), or is ``None`` when this function already
128+ assigned it.
129+
130+ * **MLX**: keep both tied on the raw ``ExportableGGUFTensor``.
131+ * **CUDA** (Q6_K or Q4_K): untie so ``lm_head`` keeps a packed low-bit matmul
132+ weight while the token embedding becomes a gatherable int8
133+ ``IntxUnpackedToInt8Tensor`` -- the truly packed int4/int6 tensors can't
134+ gather. Instead of dequantizing the whole ~1.4 B-element weight to bf16
135+ (2 B/elem), decode it once to int8 (1 B/elem; the decode is lossless so the
136+ result is numerically identical), halving the embedding's host + GPU-constant
137+ footprint. The token embedding (Q4_K for the Gemma checkpoint) is the single
138+ biggest weight, so this is the dominant saving vs the bf16 path. ``lm_head``:
139+ - Q6_K -> ``CudaDp4aPlanarInt6Tensor`` from the *same* int8 decode and
140+ assigned here (``pack_linear_for_cuda`` would mis-route an int8 tensor to
141+ the int8 path), so the post-loop resolve is a no-op.
142+ - Q4_K -> kept as the native ``Int4Tensor`` and returned, so
143+ ``_resolve_tied_lm_head`` packs it to ``CudaCoalescedInt4Tensor`` (same
144+ as a regular Q4_K linear).
145+ * **CUDA, other types**: fall back to the bf16 embedding.
146+ """
147+ if backend == "mlx" :
148+ return weight , gtensor
149+
150+ if gtensor .ggml_type in ("q6_k" , "q4_k" ):
151+ intx = gtensor .to_intx_unpacked_to_int8_tensor ()
152+ if gtensor .ggml_type == "q6_k" :
153+ import torch .nn as nn
154+ from executorch .backends .cuda .dp4a_planar_int6_tensor import (
155+ CudaDp4aPlanarInt6Tensor ,
156+ )
157+
158+ model .lm_head .weight = nn .Parameter (
159+ CudaDp4aPlanarInt6Tensor ._from_intx_int8 (intx ), requires_grad = False
160+ )
161+ return intx , None
162+ # Q4_K: ``weight`` is the native Int4Tensor; let _resolve_tied_lm_head
163+ # pack it to CudaCoalescedInt4Tensor. Only the embedding switches to int8.
164+ return intx , weight
165+
166+ from executorch .examples .models .gemma4_31b .quant import dequantize_weight
167+
168+ return dequantize_weight (weight , torch .bfloat16 ), weight
169+
170+
119171def load_gguf_model (
120172 gguf_path : str ,
121173 max_seq_len : int = 4096 ,
@@ -140,7 +192,7 @@ def load_gguf_model(
140192 Gemma4_31BConfig ,
141193 materialize_runtime_buffers ,
142194 )
143- from executorch .examples .models .gemma4_31b .quant import dequantize_weight , pack_one
195+ from executorch .examples .models .gemma4_31b .quant import pack_one
144196 from executorch .extension .llm .export .gguf import ExportableGGUFTensor , iter_gguf
145197
146198 if backend == "cuda" :
@@ -161,7 +213,7 @@ def load_gguf_model(
161213 with torch .device ("meta" ):
162214 model = Gemma4_31B (config )
163215
164- lm_head_weight = None # weight reused for a tied lm_head
216+ lm_head_weight = None # tied weight resolved into lm_head after the loop
165217 n_processed = 0
166218
167219 print (f"Streaming GGUF from { gguf_path } ..." )
@@ -173,11 +225,9 @@ def load_gguf_model(
173225 if isinstance (value , ExportableGGUFTensor ):
174226 weight = _convert_weight (model , model_key , value , backend )
175227 if model_key == "embed_tokens.weight" :
176- # Tied lm_head reuses the embedding weight: MLX wants the raw
177- # ExportableGGUFTensor (linear pattern), CUDA the quant tensor.
178- lm_head_weight = value if backend == "mlx" else weight
179- if backend == "cuda" :
180- weight = dequantize_weight (weight , torch .bfloat16 )
228+ weight , lm_head_weight = _untie_embed_lm_head (
229+ model , value , weight , backend
230+ )
181231 value = weight
182232 elif value .dtype == torch .float32 :
183233 value = value .to (torch .bfloat16 )
0 commit comments