@@ -176,6 +176,13 @@ void llama_model_glm5next::load_arch_tensors(llama_model_loader & ml) {
176176 mtp_flags |= TENSOR_SKIP ;
177177 }
178178
179+ // [fork] the draft head may also ship as its own GGUF (--model-draft + --device-draft):
180+ // blk.<n_layer>.* plus token_embd / output / output_norm and none of the trunk, so it
181+ // can be quantized apart from the trunk and pinned to the draft GPU. Both halves
182+ // declare nextn in their metadata, so probe for the tensors as qwen4exp does.
183+ const bool mtp_only = (n_layer_nextn > 0 ) && (ml.get_weight (" blk.0.attn_norm.weight" ) == nullptr );
184+ const int trunk_flags = mtp_only ? TENSOR_NOT_REQUIRED : 0 ;
185+
179186 tok_embd = create_tensor (tn (LLM_TENSOR_TOKEN_EMBD , " weight" ), {n_embd, n_vocab}, 0 );
180187 output_norm = create_tensor (tn (LLM_TENSOR_OUTPUT_NORM , " weight" ), {n_embd}, 0 );
181188 output = create_tensor (tn (LLM_TENSOR_OUTPUT , " weight" ), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED );
@@ -184,55 +191,71 @@ void llama_model_glm5next::load_arch_tensors(llama_model_loader & ml) {
184191 }
185192 // note: no hc_head_*, the hyper-connection head is a plain mean
186193
194+ // [fork, PipeDec] when the output head is pinned to the draft GPU
195+ // (--device-draft), the output norm and lm_head would follow it off the
196+ // pipeline. A graph only pipelines if it ends on an RPC backend: a local
197+ // tail makes every split-input copy fall back to synchronize + blocking
198+ // copy, which drains the endpoint inside the submission of the current
199+ // graph. Duplicate both onto the last transformer layer and use them in
200+ // the trunk/body graphs (see graph) so the whole trunk stays on the
201+ // pipeline, exactly as when there is no drafter at all. The deferred
202+ // head graph keeps the originals on the draft GPU by design.
203+ if (!mtp_only && params.mtp_dev != nullptr && n_layer > 0 ) {
204+ output_norm_trunk = create_tensor_on_layer (ml, tn (LLM_TENSOR_OUTPUT_NORM , " weight" ), {n_embd},
205+ TENSOR_NOT_REQUIRED | TENSOR_DUPLICATED , n_layer - 1 );
206+ output_trunk = create_tensor_on_layer (ml, tn (LLM_TENSOR_OUTPUT , " weight" ), {n_embd, n_vocab},
207+ TENSOR_NOT_REQUIRED | TENSOR_DUPLICATED , n_layer - 1 );
208+ }
209+
187210 for (int i = 0 ; i < n_layer_all; ++i) {
188211 auto & layer = layers[i];
189212
190213 const bool is_mtp = i >= n_layer;
191214 const bool is_recr = !is_mtp && hparams.is_recr (i);
192- const int flags = is_mtp ? mtp_flags : 0 ;
215+ const int flags = is_mtp ? mtp_flags : trunk_flags ;
193216
194217 layer.attn_norm = create_tensor (tn (LLM_TENSOR_ATTN_NORM , " weight" , i), {n_embd}, flags);
195218 layer.ffn_norm = create_tensor (tn (LLM_TENSOR_FFN_NORM , " weight" , i), {n_embd}, flags);
196219
197220 // hyper-connections wrap the trunk blocks only, the MTP block uses plain residuals
198221 if (!is_mtp) {
199- layer.hc_attn_fn = create_tensor (tn (LLM_TENSOR_HC_ATTN_FN , " weight" , i), {hc_dim, hc_mix_dim}, 0 );
200- layer.hc_attn_base = create_tensor (tn (LLM_TENSOR_HC_ATTN_BASE , " weight" , i), {hc_mix_dim}, 0 );
201- layer.hc_attn_scale = create_tensor (tn (LLM_TENSOR_HC_ATTN_SCALE , " weight" , i), {3 }, 0 );
202- layer.hc_ffn_fn = create_tensor (tn (LLM_TENSOR_HC_FFN_FN , " weight" , i), {hc_dim, hc_mix_dim}, 0 );
203- layer.hc_ffn_base = create_tensor (tn (LLM_TENSOR_HC_FFN_BASE , " weight" , i), {hc_mix_dim}, 0 );
204- layer.hc_ffn_scale = create_tensor (tn (LLM_TENSOR_HC_FFN_SCALE , " weight" , i), {3 }, 0 );
222+ layer.hc_attn_fn = create_tensor (tn (LLM_TENSOR_HC_ATTN_FN , " weight" , i), {hc_dim, hc_mix_dim}, trunk_flags );
223+ layer.hc_attn_base = create_tensor (tn (LLM_TENSOR_HC_ATTN_BASE , " weight" , i), {hc_mix_dim}, trunk_flags );
224+ layer.hc_attn_scale = create_tensor (tn (LLM_TENSOR_HC_ATTN_SCALE , " weight" , i), {3 }, trunk_flags );
225+ layer.hc_ffn_fn = create_tensor (tn (LLM_TENSOR_HC_FFN_FN , " weight" , i), {hc_dim, hc_mix_dim}, trunk_flags );
226+ layer.hc_ffn_base = create_tensor (tn (LLM_TENSOR_HC_FFN_BASE , " weight" , i), {hc_mix_dim}, trunk_flags );
227+ layer.hc_ffn_scale = create_tensor (tn (LLM_TENSOR_HC_FFN_SCALE , " weight" , i), {3 }, trunk_flags );
205228 }
206229
207230 if (is_recr) {
208231 // conv1d may be stored 4D [d_conv, 1, d_inner, 1] or 3D (quantization drops the trailing 1)
209232 auto conv = [&](llm_tensor tid) {
210233 ggml_tensor * t = create_tensor (tn (tid, " weight" , i), {d_conv, 1 , d_inner, 1 }, TENSOR_NOT_REQUIRED );
211- return t ? t : create_tensor (tn (tid, " weight" , i), {d_conv, 1 , d_inner}, 0 );
234+ return t ? t : create_tensor (tn (tid, " weight" , i), {d_conv, 1 , d_inner}, trunk_flags );
212235 };
213236 layer.ssm_q_conv = conv (LLM_TENSOR_SSM_CONV1D_Q );
214237 layer.ssm_k_conv = conv (LLM_TENSOR_SSM_CONV1D_K );
215238 layer.ssm_v_conv = conv (LLM_TENSOR_SSM_CONV1D_V );
216239
217- create_tensor_qkv (layer, i, n_embd, d_inner, d_inner, d_inner, 0 );
240+ create_tensor_qkv (layer, i, n_embd, d_inner, d_inner, d_inner, trunk_flags );
218241
219- layer.ssm_f_a = create_tensor (tn (LLM_TENSOR_SSM_F_A , " weight" , i), {n_embd, head_dim}, 0 );
220- layer.ssm_f_b = create_tensor (tn (LLM_TENSOR_SSM_F_B , " weight" , i), {head_dim, d_inner}, 0 );
221- layer.ssm_g_a = create_tensor (tn (LLM_TENSOR_SSM_G_A , " weight" , i), {n_embd, head_dim}, 0 );
222- layer.ssm_g_b = create_tensor (tn (LLM_TENSOR_SSM_G_B , " weight" , i), {head_dim, d_inner}, 0 );
223- layer.ssm_beta = create_tensor (tn (LLM_TENSOR_SSM_BETA , " weight" , i), {n_embd, n_head}, 0 );
242+ layer.ssm_f_a = create_tensor (tn (LLM_TENSOR_SSM_F_A , " weight" , i), {n_embd, head_dim}, trunk_flags );
243+ layer.ssm_f_b = create_tensor (tn (LLM_TENSOR_SSM_F_B , " weight" , i), {head_dim, d_inner}, trunk_flags );
244+ layer.ssm_g_a = create_tensor (tn (LLM_TENSOR_SSM_G_A , " weight" , i), {n_embd, head_dim}, trunk_flags );
245+ layer.ssm_g_b = create_tensor (tn (LLM_TENSOR_SSM_G_B , " weight" , i), {head_dim, d_inner}, trunk_flags );
246+ layer.ssm_beta = create_tensor (tn (LLM_TENSOR_SSM_BETA , " weight" , i), {n_embd, n_head}, trunk_flags );
224247
225248 // ssm_a holds -exp(A_log), folded at conversion time (kimi-linear/kimi-k3 convention)
226- layer.ssm_a = create_tensor (tn (LLM_TENSOR_SSM_A , i), {n_head}, 0 );
249+ layer.ssm_a = create_tensor (tn (LLM_TENSOR_SSM_A , i), {n_head}, trunk_flags );
227250
228251 // some converters emit dt_bias under the default ".weight" suffix
229252 layer.ssm_dt_b = create_tensor (tn (LLM_TENSOR_SSM_DT , " bias" , i), {d_inner}, TENSOR_NOT_REQUIRED );
230253 if (!layer.ssm_dt_b ) {
231- layer.ssm_dt_b = create_tensor (tn (LLM_TENSOR_SSM_DT , " weight" , i), {d_inner}, 0 );
254+ layer.ssm_dt_b = create_tensor (tn (LLM_TENSOR_SSM_DT , " weight" , i), {d_inner}, trunk_flags );
232255 }
233256
234- layer.ssm_o_norm = create_tensor (tn (LLM_TENSOR_SSM_NORM , " weight" , i), {head_dim}, 0 );
235- layer.wo = create_tensor (tn (LLM_TENSOR_ATTN_OUT , " weight" , i), {d_inner, n_embd}, 0 );
257+ layer.ssm_o_norm = create_tensor (tn (LLM_TENSOR_SSM_NORM , " weight" , i), {head_dim}, trunk_flags );
258+ layer.wo = create_tensor (tn (LLM_TENSOR_ATTN_OUT , " weight" , i), {d_inner, n_embd}, trunk_flags );
236259 } else {
237260 layer.wq_a = create_tensor (tn (LLM_TENSOR_ATTN_Q_A , " weight" , i), {n_embd, q_lora_rank}, flags);
238261 layer.attn_q_a_norm = create_tensor (tn (LLM_TENSOR_ATTN_Q_A_NORM , " weight" , i), {q_lora_rank}, flags);
@@ -292,6 +315,10 @@ void llama_model_glm5next::load_arch_tensors(llama_model_loader & ml) {
292315}
293316
294317std::unique_ptr<llm_graph_context> llama_model_glm5next::build_arch_graph (const llm_graph_params & params) const {
318+ if (params.gtype == LLM_GRAPH_TYPE_DECODER_PIPEDEC_HEAD ) {
319+ return std::make_unique<graph_pipedec_head>(*this , params);
320+ }
321+
295322 if (params.gtype == LLM_GRAPH_TYPE_DECODER_MTP ) {
296323 return std::make_unique<graph_mtp>(*this , params);
297324 }
@@ -991,7 +1018,13 @@ llama_model_glm5next::graph::graph(const llama_model & model, const llm_graph_pa
9911018 // the k-pool metadata is the same for every full layer, so build it once
9921019 auto * inp_kpool = build_inp_kpool (inp_hyb);
9931020
994- ggml_tensor * inp_out_ids = build_inp_out_ids ();
1021+ // The PipeDec body returns every unmasked hidden row directly and never
1022+ // gathers output rows. Do not register an unused out_ids input: unused
1023+ // graph inputs are intentionally not allocated, but set_inputs() would
1024+ // still attempt to populate one if it were registered here.
1025+ ggml_tensor * inp_out_ids = params.gtype == LLM_GRAPH_TYPE_DECODER_PIPEDEC_BODY
1026+ ? nullptr
1027+ : build_inp_out_ids ();
9951028
9961029 // inputs_embeds.unsqueeze(2).expand(-1, -1, hc, -1)
9971030 ggml_tensor * inpL = ggml_reshape_3d (ctx0, inp, n_embd, 1 , n_tokens);
@@ -1053,19 +1086,58 @@ llama_model_glm5next::graph::graph(const llama_model & model, const llm_graph_pa
10531086 ggml_tensor * cur = glm5next_hc_mean (ctx0, inpL);
10541087 cb (cur, " hc_head" , -1 );
10551088
1056- cur = build_norm (cur, model.output_norm , nullptr , LLM_NORM_RMS , -1 );
1089+ const auto & model_glm = static_cast <const llama_model_glm5next &>(model);
1090+
1091+ cur = build_norm (cur, model_glm.output_norm_trunk ? model_glm.output_norm_trunk : model.output_norm ,
1092+ nullptr , LLM_NORM_RMS , -1 );
10571093
10581094 // post-norm hidden state feeds the NextN/MTP draft head
10591095 cb (cur, " h_nextn" , -1 );
10601096 res->t_h_nextn = cur;
10611097
1098+ // [fork, PipeDec] stage 2 queues token-sized trunk graphs across the layer
1099+ // devices. The body ends at the post-output-norm hidden state; those rows
1100+ // are copied out and fed to one batched graph_pipedec_head (lm_head only)
1101+ // after the body pipeline drains. Stopping here also keeps the body graph
1102+ // on the pipeline: the lm_head would otherwise drag the tail to the draft
1103+ // GPU and end the graph on a local backend.
1104+ if (params.gtype == LLM_GRAPH_TYPE_DECODER_PIPEDEC_BODY ) {
1105+ ggml_build_forward_expand (gf, cur);
1106+ return ;
1107+ }
1108+
10621109 if (inp_out_ids && narrow_late) {
10631110 cur = ggml_get_rows (ctx0, cur, inp_out_ids);
10641111 }
10651112
10661113 cb (cur, " result_norm" , -1 );
10671114 res->t_embd = cur;
10681115
1116+ cur = ggml_mul_mat (ctx0, model_glm.output_trunk ? model_glm.output_trunk : model.output , cur);
1117+ cb (cur, " result_output" , -1 );
1118+ res->t_logits = cur;
1119+
1120+ ggml_build_forward_expand (gf, cur);
1121+ }
1122+
1123+ // Deferred PipeDec verification head: the body lanes stop at the post-norm
1124+ // hidden state, so all that is left here is the lm_head over the batched rows
1125+ // gathered from every lane in the group. Runs on the draft GPU by design.
1126+ llama_model_glm5next::graph_pipedec_head::graph_pipedec_head (
1127+ const llama_model & model, const llm_graph_params & params)
1128+ : llm_graph_context(params) {
1129+ auto inp = std::make_unique<llm_graph_input_embd>(hparams.n_embd );
1130+
1131+ inp->embd = ggml_new_tensor_2d (ctx0, GGML_TYPE_F32 , hparams.n_embd , n_tokens);
1132+ ggml_set_input (inp->embd );
1133+ cb (inp->embd , " pipedec_h_input" , -1 );
1134+
1135+ ggml_tensor * cur = inp->embd ;
1136+ res->add_input (std::move (inp));
1137+
1138+ cb (cur, " result_norm" , -1 );
1139+ res->t_embd = cur;
1140+
10691141 cur = ggml_mul_mat (ctx0, model.output , cur);
10701142 cb (cur, " result_output" , -1 );
10711143 res->t_logits = cur;
0 commit comments