char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aman Guptaqwen4exp: enable rms_norm + mul fusion (#28896)41abbfd59

char/aliceai-t5-moe
58.3 KiB1280 linesraw
1#include "models.h"
2#include "llama-impl.h"
3#include "llama-memory-hybrid-idx.h"
4#include "llama-memory-recurrent.h"
5
6#include <algorithm>
7#include <cinttypes>
8
9// bad metadata must be catchable: GGML_ASSERT aborts the whole process
10static void qwen4exp_require_nonzero(const llama_model_loader & ml, llm_kv kid, uint32_t value) {
11    if (value == 0) {
12        throw std::runtime_error(format("%s must be greater than zero, got %u", ml.llm_kv(kid).c_str(), value));
13    }
14}
15
16// get_arr() copies a short array as-is, leaving a zero tail the n-gram hash silently drops
17static void qwen4exp_require_arr_len(llama_model_loader & ml, llm_kv kid, uint32_t n_min) {
18    uint32_t n_arr = 0;
19    ml.get_arr_n(kid, n_arr, true);
20    if (n_arr < n_min) {
21        throw std::runtime_error(format("%s has %u entries, but at least %u are required",
22                                        ml.llm_kv(kid).c_str(), n_arr, n_min));
23    }
24}
25
26void llama_model_qwen4exp::load_arch_hparams(llama_model_loader & ml) {
27    ml.get_key_or_arr(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp_arr, hparams.n_layer_all, false);
28    ml.get_key(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp, false);
29    ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS,       hparams.f_norm_rms_eps);
30
31    ml.get_key_or_arr(LLM_KV_ROPE_DIMENSION_SECTIONS,    hparams.rope_sections, 4, true);
32
33    ml.get_key(LLM_KV_SSM_CONV_KERNEL,    hparams.ssm_d_conv);
34    ml.get_key(LLM_KV_SSM_INNER_SIZE,     hparams.ssm_d_inner);
35    ml.get_key(LLM_KV_SSM_STATE_SIZE,     hparams.ssm_d_state);
36    ml.get_key(LLM_KV_SSM_TIME_STEP_RANK, hparams.ssm_dt_rank);
37    ml.get_key(LLM_KV_SSM_GROUP_COUNT,    hparams.ssm_n_group);
38    qwen4exp_require_nonzero(ml, LLM_KV_SSM_CONV_KERNEL,    hparams.ssm_d_conv);
39    qwen4exp_require_nonzero(ml, LLM_KV_SSM_INNER_SIZE,     hparams.ssm_d_inner);
40    qwen4exp_require_nonzero(ml, LLM_KV_SSM_STATE_SIZE,     hparams.ssm_d_state);
41    qwen4exp_require_nonzero(ml, LLM_KV_SSM_TIME_STEP_RANK, hparams.ssm_dt_rank);
42    qwen4exp_require_nonzero(ml, LLM_KV_SSM_GROUP_COUNT,    hparams.ssm_n_group);
43
44    // HC; low_rank is qwen4exp-specific, DeepSeek-V4 leaves it absent (full rank)
45    ml.get_key(LLM_KV_HYPER_CONNECTION_COUNT,    hparams.dsv4_hc_mult);
46    ml.get_key(LLM_KV_HYPER_CONNECTION_LOW_RANK, hparams.hc_low_rank);
47    // a count of 1 has nothing to mix: transformers configuration_qwen4_exp.py:196, vLLM
48    // config.py:49 and SGLang configs/qwen4_exp.py:38 all raise on hc_count <= 1
49    if (hparams.dsv4_hc_mult <= 1) {
50        throw std::runtime_error(format("%s must be greater than one, got %u",
51                                        ml.llm_kv(LLM_KV_HYPER_CONNECTION_COUNT).c_str(), hparams.dsv4_hc_mult));
52    }
53    qwen4exp_require_nonzero(ml, LLM_KV_HYPER_CONNECTION_LOW_RANK, hparams.hc_low_rank);
54    hparams.n_embd_out_impl = hparams.dsv4_hc_mult * hparams.n_embd;
55
56    ml.get_key(LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, hparams.indexer_n_head);
57    ml.get_key(LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, hparams.indexer_head_size);
58    ml.get_key(LLM_KV_ATTENTION_INDEXER_TOP_K,      hparams.indexer_top_k);
59    qwen4exp_require_nonzero(ml, LLM_KV_ATTENTION_INDEXER_HEAD_COUNT, hparams.indexer_n_head);
60    qwen4exp_require_nonzero(ml, LLM_KV_ATTENTION_INDEXER_KEY_LENGTH, hparams.indexer_head_size);
61    qwen4exp_require_nonzero(ml, LLM_KV_ATTENTION_INDEXER_TOP_K,      hparams.indexer_top_k);
62    ml.get_key_or_arr(LLM_KV_ATTENTION_COMPRESS_RATIOS, hparams.dsv4_compress_ratios, hparams.n_layer_all, false);
63
64    // PLE n-gram hash embeddings; if the key group is absent every field stays zero
65    hparams.is_ple_impl.reset();
66    hparams.ple_n_heads = 0;
67
68    uint32_t n_ple = 0;
69    ml.get_arr_n(LLM_KV_PLE_LAYERS, n_ple, false);
70    if (n_ple > 0) {
71        std::vector<uint32_t> ple_layers;
72        ml.get_arr(LLM_KV_PLE_LAYERS, ple_layers);
73        if (n_ple != 1) {
74            // hparams holds one set of hash constants, so several PLE modules cannot be represented
75            throw std::runtime_error(format("%s lists %u layers, but only one PLE layer is supported",
76                                            ml.llm_kv(LLM_KV_PLE_LAYERS).c_str(), n_ple));
77        }
78        for (uint32_t il : ple_layers) {
79            if (il >= hparams.n_layer_all) {
80                throw std::runtime_error(format("PLE layer %u is out of range", il));
81            }
82            hparams.is_ple_impl.set(il);
83        }
84
85        ml.get_key(LLM_KV_PLE_NGRAM_SIZE,      hparams.ple_ngram_size);
86        ml.get_key(LLM_KV_PLE_HEADS_PER_NGRAM, hparams.ple_heads_per_ngram);
87        ml.get_key(LLM_KV_PLE_CONV_KERNEL,     hparams.ple_conv_kernel);
88        ml.get_key(LLM_KV_PLE_EOS_TOKEN_ID,    hparams.ple_eos_token_id);
89        // optional: files written before this key fall back to the EOS token
90        ml.get_key(LLM_KV_PLE_IMAGE_TOKEN_ID,  hparams.ple_image_token_id, false);
91        ml.get_key(LLM_KV_EMBEDDING_LENGTH_PER_LAYER, hparams.n_embd_per_layer);
92        qwen4exp_require_nonzero(ml, LLM_KV_PLE_CONV_KERNEL,             hparams.ple_conv_kernel);
93        qwen4exp_require_nonzero(ml, LLM_KV_EMBEDDING_LENGTH_PER_LAYER,  hparams.n_embd_per_layer);
94
95        hparams.ple_n_heads  = (hparams.ple_ngram_size - 1) * hparams.ple_heads_per_ngram;
96        hparams.ple_head_dim = hparams.n_embd_per_layer;
97        if (hparams.ple_ngram_size < 2 || hparams.ple_ngram_size > LLAMA_MAX_PLE_NGRAM) {
98            throw std::runtime_error(format("PLE n-gram size %u is out of range", hparams.ple_ngram_size));
99        }
100        if (hparams.ple_n_heads == 0 || hparams.ple_n_heads > LLAMA_MAX_PLE_HEADS) {
101            throw std::runtime_error(format("PLE head count %u is out of range", hparams.ple_n_heads));
102        }
103
104        qwen4exp_require_arr_len(ml, LLM_KV_PLE_LAYER_MULTIPLIERS, hparams.ple_ngram_size);
105        qwen4exp_require_arr_len(ml, LLM_KV_PLE_HEAD_OFFSETS,      hparams.ple_n_heads);
106        qwen4exp_require_arr_len(ml, LLM_KV_PLE_HEAD_VOCAB_SIZES,  hparams.ple_n_heads);
107
108        ml.get_arr(LLM_KV_PLE_LAYER_MULTIPLIERS, hparams.ple_layer_multipliers);
109
110        // the file stores the head ranges as uint64, so read at that width and narrow to the int32 the gather uses
111        std::array<uint64_t, LLAMA_MAX_PLE_HEADS> head_offsets     = {};
112        std::array<uint64_t, LLAMA_MAX_PLE_HEADS> head_vocab_sizes = {};
113        ml.get_arr(LLM_KV_PLE_HEAD_OFFSETS,     head_offsets);
114        ml.get_arr(LLM_KV_PLE_HEAD_VOCAB_SIZES, head_vocab_sizes);
115        for (uint32_t h = 0; h < hparams.ple_n_heads; ++h) {
116            if (head_vocab_sizes[h] == 0 ||
117                head_offsets[h]     > INT32_MAX ||
118                head_vocab_sizes[h] > INT32_MAX ||
119                head_offsets[h] + head_vocab_sizes[h] > INT32_MAX) {
120                throw std::runtime_error(format("PLE head %u range does not fit the int32 row index", h));
121            }
122            hparams.ple_head_offsets[h]     = (uint32_t) head_offsets[h];
123            hparams.ple_head_vocab_sizes[h] = (uint32_t) head_vocab_sizes[h];
124        }
125    }
126
127    // linear attention everywhere except every full_attention_interval-th layer
128    if (!ml.get_key_or_arr(LLM_KV_ATTENTION_RECURRENT_LAYERS, hparams.is_recr_impl, hparams.n_layer_all, false)) {
129        uint32_t full_attn_interval = 4;
130        ml.get_key(LLM_KV_FULL_ATTENTION_INTERVAL, full_attn_interval, false);
131        qwen4exp_require_nonzero(ml, LLM_KV_FULL_ATTENTION_INTERVAL, full_attn_interval);
132        for (uint32_t i = 0; i < hparams.n_layer_all; ++i) {
133            hparams.is_recr_impl[i] = (i < hparams.n_layer()) && ((i + 1) % full_attn_interval != 0);
134        }
135    }
136
137    // the PLE conv history is a row of the recurrent cache, which linear layers alone have
138    for (uint32_t i = 0; i < hparams.n_layer_all; ++i) {
139        if (hparams.is_ple(i) && !hparams.is_recr(i)) {
140            throw std::runtime_error(format("PLE layer %u is not a linear attention layer", i));
141        }
142    }
143
144    switch (hparams.n_layer()) {
145        case 48: type = LLM_TYPE_A3B; break;
146        default: type = LLM_TYPE_UNKNOWN;
147    }
148}
149
150void llama_model_qwen4exp::load_arch_tensors(llama_model_loader & ml) {
151    LLAMA_LOAD_LOCALS;
152
153    const int64_t hc     = hparams.dsv4_hc_mult;
154    const int64_t hc_dim = hc * n_embd;
155    const int64_t hc_lr  = hparams.hc_low_rank;
156
157    tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), { n_embd, n_vocab }, 0);
158
159    // there is no output_norm: the final hyper-connection mixer carries it
160    // the gammas load as [n_embd, hc] so the grouped norm multiplies them without a graph reshape
161    hc_head_norm = create_tensor(tn(LLM_TENSOR_HC_HEAD_NORM, "weight"), { n_embd, hc }, TENSOR_ALLOW_RESHAPE);
162    hc_head_down = create_tensor(tn(LLM_TENSOR_HC_HEAD_DOWN, "weight"), { hc_dim, hc_lr }, 0);
163    hc_head_up   = create_tensor(tn(LLM_TENSOR_HC_HEAD_UP,   "weight"), { hc_lr, hc_dim }, 0);
164
165    output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), { n_embd, n_vocab }, TENSOR_NOT_REQUIRED);
166    if (output == NULL) {
167        output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), { n_embd, n_vocab }, TENSOR_DUPLICATED);
168    }
169
170    // flat [ple_head_dim, n_rows] gather target
171    if (hparams.ple_n_heads > 0) {
172        // the head ranges are what the gather indexes, so they set the minimum row count
173        int64_t ple_rows = 0;
174        for (uint32_t h = 0; h < hparams.ple_n_heads; ++h) {
175            ple_rows = std::max(ple_rows, (int64_t) hparams.ple_head_offsets[h] + hparams.ple_head_vocab_sizes[h]);
176        }
177
178        // the converter pads the table; a model synthesised from metadata has no tensor to ask
179        const std::string ple_name = tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight").str();
180        if (const auto * ple_w = ml.get_weight(ple_name.c_str())) {
181            if (ple_w->tensor->ne[1] < ple_rows) {
182                throw std::runtime_error(format("%s has %" PRId64 " rows, too few for the PLE head ranges (%" PRId64 ")",
183                                                ple_name.c_str(), ple_w->tensor->ne[1], ple_rows));
184            }
185            ple_rows = ple_w->tensor->ne[1];
186        }
187
188        per_layer_tok_embd = create_tensor(tn(LLM_TENSOR_PER_LAYER_TOKEN_EMBD, "weight"),
189                                           { hparams.ple_head_dim, ple_rows }, TENSOR_READ_LAZY);
190    }
191
192    for (int il = 0; il < n_layer; ++il) {
193        auto & layer = layers[il];
194
195        const int64_t n_ff_exp   = hparams.n_ff_exp() ? hparams.n_ff_exp() : n_ff / n_expert_used;
196        const int64_t n_ff_shexp = hparams.n_ff_shexp ? hparams.n_ff_shexp : n_ff;
197
198        const int64_t head_k_dim = hparams.ssm_d_state;
199        const int64_t head_v_dim = hparams.ssm_d_state;
200        const int64_t n_k_heads  = hparams.ssm_n_group;
201        const int64_t n_v_heads  = hparams.ssm_dt_rank;
202        const int64_t key_dim    = head_k_dim * n_k_heads;
203        const int64_t value_dim  = head_v_dim * n_v_heads;
204        const int64_t conv_dim   = key_dim * 2 + value_dim;
205
206        // two HC modules per layer: before the token mixer, before the MoE
207        layer.hc_attn_norm   = create_tensor(tn(LLM_TENSOR_HC_ATTN_NORM,   "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE);
208        layer.hc_attn_down   = create_tensor(tn(LLM_TENSOR_HC_ATTN_DOWN,   "weight", il), { hc_dim, hc_lr }, 0);
209        layer.hc_attn_up     = create_tensor(tn(LLM_TENSOR_HC_ATTN_UP,     "weight", il), { hc_lr, hc_dim }, 0);
210        layer.hc_attn_inject = create_tensor(tn(LLM_TENSOR_HC_ATTN_INJECT, "weight", il), { hc_dim, hc }, 0);
211        layer.hc_ffn_norm    = create_tensor(tn(LLM_TENSOR_HC_FFN_NORM,    "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE);
212        layer.hc_ffn_down    = create_tensor(tn(LLM_TENSOR_HC_FFN_DOWN,    "weight", il), { hc_dim, hc_lr }, 0);
213        layer.hc_ffn_up      = create_tensor(tn(LLM_TENSOR_HC_FFN_UP,      "weight", il), { hc_lr, hc_dim }, 0);
214        layer.hc_ffn_inject  = create_tensor(tn(LLM_TENSOR_HC_FFN_INJECT,  "weight", il), { hc_dim, hc }, 0);
215
216        if (!hparams.is_recr(il)) {
217            // full attention: wq holds [q|gate] interleaved per head
218            create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, 0);
219            layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, 0);
220
221            layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, 0);
222            layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, 0);
223
224            const int64_t idx_dim = hparams.indexer_head_size;
225            layer.index_q_proj = create_tensor(tn(LLM_TENSOR_INDEXER_Q_PROJ, "weight", il), { n_embd, hparams.indexer_n_head * idx_dim }, 0);
226            layer.index_k_proj = create_tensor(tn(LLM_TENSOR_INDEXER_K_PROJ, "weight", il), { n_embd, idx_dim }, 0);
227            layer.index_q_norm = create_tensor(tn(LLM_TENSOR_INDEXER_Q_NORM, "weight", il), { idx_dim }, 0);
228            layer.index_k_norm = create_tensor(tn(LLM_TENSOR_INDEXER_K_NORM, "weight", il), { idx_dim }, 0);
229        } else {
230            layer.wqkv       = create_tensor(tn(LLM_TENSOR_ATTN_QKV,   "weight", il), { n_embd, key_dim * 2 + value_dim }, 0);
231            layer.wqkv_gate  = create_tensor(tn(LLM_TENSOR_ATTN_GATE,  "weight", il), { n_embd, value_dim }, 0);
232            layer.ssm_conv1d = create_tensor(tn(LLM_TENSOR_SSM_CONV1D, "weight", il), { hparams.ssm_d_conv, conv_dim }, 0);
233            layer.ssm_dt     = create_tensor(tn(LLM_TENSOR_SSM_DT,     "bias",   il), { hparams.ssm_dt_rank }, 0);
234            layer.ssm_a      = create_tensor(tn(LLM_TENSOR_SSM_A_NOSCAN,         il), { hparams.ssm_dt_rank }, 0);
235            layer.ssm_beta   = create_tensor(tn(LLM_TENSOR_SSM_BETA,   "weight", il), { n_embd, n_v_heads }, 0);
236            layer.ssm_alpha  = create_tensor(tn(LLM_TENSOR_SSM_ALPHA,  "weight", il), { n_embd, n_v_heads }, 0);
237            layer.ssm_norm   = create_tensor(tn(LLM_TENSOR_SSM_NORM,   "weight", il), { head_v_dim }, 0);
238            layer.ssm_out    = create_tensor(tn(LLM_TENSOR_SSM_OUT,    "weight", il), { value_dim, n_embd }, 0);
239        }
240
241        if (hparams.is_ple(il)) {
242            layer.ple_key        = create_tensor(tn(LLM_TENSOR_PLE_KEY,        "weight", il), { n_embd, hc_dim }, 0);
243            layer.ple_value      = create_tensor(tn(LLM_TENSOR_PLE_VALUE,      "weight", il), { n_embd, n_embd }, 0);
244            layer.ple_norm_key   = create_tensor(tn(LLM_TENSOR_PLE_NORM_KEY,   "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE);
245            layer.ple_norm_query = create_tensor(tn(LLM_TENSOR_PLE_NORM_QUERY, "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE);
246            layer.ple_norm_conv  = create_tensor(tn(LLM_TENSOR_PLE_NORM_CONV,  "weight", il), { n_embd, hc }, TENSOR_ALLOW_RESHAPE);
247            layer.ple_conv1d     = create_tensor(tn(LLM_TENSOR_PLE_CONV1D,     "weight", il), { hparams.ple_conv_kernel, hc_dim }, 0);
248        }
249
250        layer.ffn_gate_inp  = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP,  "weight", il), { n_embd, n_expert }, 0);
251        layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", il), { n_ff_exp, n_embd, n_expert }, 0);
252        create_tensor_gate_up_exps(layer, il, n_embd, n_ff_exp, n_expert, 0);
253
254        layer.ffn_gate_inp_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP_SHEXP, "weight", il), { n_embd }, 0);
255        layer.ffn_gate_shexp     = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP,     "weight", il), { n_embd, n_ff_shexp }, 0);
256        layer.ffn_up_shexp       = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP,       "weight", il), { n_embd, n_ff_shexp }, 0);
257        layer.ffn_down_shexp     = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP,     "weight", il), { n_ff_shexp, n_embd }, 0);
258    }
259}
260
261std::unique_ptr<llm_graph_context> llama_model_qwen4exp::build_arch_graph(const llm_graph_params & params) const {
262    return std::make_unique<graph>(*this, params);
263}
264
265// Hyper-connections keep hc parallel residual streams [n_embd, hc, T] in place of layer norms.
266// Returns the mixed [n_embd, T] stream; `inject` gets the [hc, T] scatter weights.
267ggml_tensor * llama_model_qwen4exp::graph::build_hc_mix(
268        ggml_tensor *  x,
269        ggml_tensor *  w_norm,
270        ggml_tensor *  w_down,
271        ggml_tensor *  w_up,
272        ggml_tensor *  w_inject,
273        ggml_tensor ** inject,
274        int            il) {
275    const int64_t hc     = hparams.dsv4_hc_mult;
276    const int64_t hc_dim = hc * n_embd;
277    const int64_t nt     = x->ne[2];
278
279    // grouped RMSNorm: reduce over one stream, then scale all streams with the [n_embd, hc] gamma
280    // the converter folded each gamma to (1 + w)
281    ggml_tensor * xn = ggml_mul(ctx0, ggml_rms_norm(ctx0, x, hparams.f_norm_rms_eps), w_norm);
282    xn = ggml_reshape_2d(ctx0, xn, hc_dim, nt);
283    cb(xn, "hc_norm", il);
284
285    ggml_tensor * lo = build_lora_mm(w_down, xn);
286    lo = ggml_silu(ctx0, ggml_scale(ctx0, lo, 1.0f / (float) hc));
287    ggml_tensor * gate = ggml_sigmoid(ctx0, build_lora_mm(w_up, lo));
288    cb(gate, "hc_gate", il);
289
290    ggml_tensor * gated = ggml_mul(ctx0, xn, gate);
291    gated = ggml_reshape_3d(ctx0, gated, n_embd, hc, nt);
292
293    // collapse the streams by their mean
294    ggml_tensor * mixed = ggml_view_2d(ctx0, gated, n_embd, nt,
295            ggml_row_size(gated->type, n_embd) * hc, 0);
296    mixed = ggml_cont(ctx0, mixed);
297    for (int64_t c = 1; c < hc; ++c) {
298        ggml_tensor * s = ggml_view_2d(ctx0, gated, n_embd, nt,
299                ggml_row_size(gated->type, n_embd) * hc,
300                ggml_row_size(gated->type, n_embd) * c);
301        mixed = ggml_add(ctx0, mixed, s);
302    }
303    mixed = ggml_scale(ctx0, mixed, 1.0f / (float) hc);
304    cb(mixed, "hc_mixed", il);
305
306    if (inject) {
307        *inject = build_lora_mm(w_inject, xn);
308        cb(*inject, "hc_inject", il);
309    }
310
311    return mixed;
312}
313
314ggml_tensor * llama_model_qwen4exp::graph::build_hc_combine(
315        ggml_tensor * residual,
316        ggml_tensor * block_out,
317        ggml_tensor * inject,
318        int           il) {
319    const int64_t hc = hparams.dsv4_hc_mult;
320    const int64_t nt = residual->ne[2];
321
322    // 2*sigmoid centres the scatter weights on 1, so a zero injection is a plain residual add
323    ggml_tensor * w = ggml_sigmoid(ctx0, ggml_scale(ctx0, inject, 1.0f / (float) hc));
324    w = ggml_scale(ctx0, w, 2.0f);
325    w = ggml_reshape_3d(ctx0, w, 1, hc, nt);
326
327    ggml_tensor * b = ggml_reshape_3d(ctx0, block_out, n_embd, 1, nt);
328    b = ggml_repeat_4d(ctx0, b, n_embd, hc, nt, 1);
329
330    ggml_tensor * cur = ggml_add(ctx0, residual, ggml_mul(ctx0, b, w));
331    cb(cur, "hc_combine", il);
332
333    return cur;
334}
335
336llama_model_qwen4exp::graph::graph(const llama_model & model, const llm_graph_params & params) :
337    llm_build_delta_net_base(params), model(model) {
338    const int64_t hc = hparams.dsv4_hc_mult;
339
340    GGML_ASSERT(hparams.n_embd_head_v() == hparams.n_embd_head_k());
341
342    int sections[4];
343    std::copy(std::begin(hparams.rope_sections), std::begin(hparams.rope_sections) + 4, sections);
344
345    ggml_tensor * inpL = build_inp_embd(model.tok_embd);
346    cb(inpL, "model.input_embed", -1);
347    ggml_build_forward_expand(gf, inpL);
348
349    auto * inp = build_inp_mem_hybrid();
350
351    // qwen4exp always builds llama_memory_hybrid_idx, so this downcast is safe
352    // the indexer cache inside it is absent when the GGUF has no indexer tensors
353    const auto * mctx_hyb = static_cast<const llama_memory_hybrid_idx_context *>(inp->mctx);
354
355    const llama_kv_cache_context * mctx_idx = mctx_hyb->get_idx();
356    if (mctx_idx) {
357        GGML_ASSERT(mctx_idx->get_n_kv() == inp->mctx->get_attn()->get_n_kv() &&
358                "the indexer cache must track the attention cache cell for cell");
359    }
360
361    ggml_tensor * inp_pos     = build_inp_pos();
362    ggml_tensor * inp_out_ids = build_inp_out_ids();
363
364    ggml_tensor * ple_emb = nullptr;
365    if (hparams.ple_n_heads > 0) {
366        ple_emb = build_inp_ple(mctx_hyb);
367        // make sure ple_emb and build_inp_embd are in the same graph split
368        ggml_build_forward_expand(gf, ple_emb);
369    }
370
371    // the wide residual starts as hc identical copies of the embedding
372    ggml_tensor * res_hc = ggml_repeat_4d(ctx0,
373            ggml_reshape_3d(ctx0, inpL, n_embd, 1, n_tokens),
374            n_embd, hc, n_tokens, 1);
375    cb(res_hc, "hc_init", -1);
376
377    for (int il = 0; il < n_layer; ++il) {
378        res->t_layer_inp[il] = res_hc;
379
380        if (hparams.is_ple(il)) {
381            res_hc = build_ple(inp->get_recr(), ple_emb, res_hc, il);
382        }
383
384        ggml_tensor * inject = nullptr;
385        ggml_tensor * cur = build_hc_mix(res_hc,
386                model.layers[il].hc_attn_norm,
387                model.layers[il].hc_attn_down,
388                model.layers[il].hc_attn_up,
389                model.layers[il].hc_attn_inject,
390                &inject, il);
391
392        ggml_build_forward_expand(gf, cur);
393
394        if (hparams.is_recr(il)) {
395            cur = build_layer_attn_linear(inp->get_recr(), cur, il);
396        } else {
397            cur = build_layer_attn(inp->get_attn(), mctx_hyb, cur, inp_pos, sections, il);
398        }
399
400        if (il == n_layer - 1 && inp_out_ids) {
401            // everything below is per token, so drop the rows that produce no output
402            cur    = ggml_get_rows(ctx0, cur,    inp_out_ids);
403            inject = ggml_get_rows(ctx0, inject, inp_out_ids);
404
405            res_hc = ggml_reshape_2d(ctx0, res_hc, n_embd*hc, res_hc->ne[2]);
406            res_hc = ggml_get_rows(ctx0, res_hc, inp_out_ids);
407            res_hc = ggml_reshape_3d(ctx0, res_hc, n_embd, hc, res_hc->ne[1]);
408        }
409
410        res_hc = build_hc_combine(res_hc, cur, inject, il);
411
412        cur = build_hc_mix(res_hc,
413                model.layers[il].hc_ffn_norm,
414                model.layers[il].hc_ffn_down,
415                model.layers[il].hc_ffn_up,
416                model.layers[il].hc_ffn_inject,
417                &inject, il);
418
419        cur = build_layer_ffn(cur, il);
420        cb(cur, "ffn_out", il);
421
422        res_hc = build_hc_combine(res_hc, cur, inject, il);
423
424        // "l_last" is the layer output name that build_cvec and imatrix look for
425        cb(res_hc, "l_last", il);
426    }
427
428    // the final mixer is the output norm: there is no separate one
429    ggml_tensor * cur = build_hc_mix(res_hc,
430            model.hc_head_norm, model.hc_head_down, model.hc_head_up,
431            nullptr, nullptr, -1);
432
433    cb(cur, "result_norm", -1);
434    res->t_embd = cur;
435
436    cur = build_lora_mm(model.output, cur, model.output_s);
437    cb(cur, "result_output", -1);
438    res->t_logits = cur;
439
440    ggml_build_forward_expand(gf, cur);
441}
442
443std::pair<ggml_tensor *, ggml_tensor *> llama_model_qwen4exp::graph::build_qkvz(
444                ggml_tensor * input,
445                        int   il) {
446    const int64_t n_seqs       = ubatch.n_seqs;
447    const int64_t n_seq_tokens = ubatch.n_seq_tokens;
448
449    ggml_tensor * qkv_mixed = build_lora_mm(model.layers[il].wqkv, input, model.layers[il].wqkv_s);
450    qkv_mixed = ggml_reshape_3d(ctx0, qkv_mixed, qkv_mixed->ne[0], n_seq_tokens, n_seqs);
451    cb(qkv_mixed, "linear_attn_qkv_mixed", il);
452
453    ggml_tensor * z = build_lora_mm(model.layers[il].wqkv_gate, input, model.layers[il].wqkv_gate_s);
454    cb(z, "z", il);
455
456    return { qkv_mixed, z };
457}
458
459ggml_tensor * llama_model_qwen4exp::graph::build_norm_gated(
460        ggml_tensor * input,
461        ggml_tensor * weights,
462        ggml_tensor * gate,
463        int           layer) {
464    // the one numerical difference from Qwen3.5's GDN: sigmoid output gate, not silu
465    ggml_tensor * normalized = build_norm(input, weights, nullptr, LLM_NORM_RMS, layer);
466    ggml_tensor * gated = ggml_sigmoid(ctx0, gate);
467
468    return ggml_mul(ctx0, normalized, gated);
469}
470
471// QSA attends to a budget of whole blocks of compress_ratio tokens, plus the incomplete tail
472// one mean-pooled indexer key scores each block; set_input resolves the cache layout
473class llama_model_qwen4exp::llm_graph_input_qsa : public llm_graph_input_i {
474public:
475    llm_graph_input_qsa(const llama_memory_hybrid_idx_context * mctx, uint32_t ratio, bool blk_bias) :
476        mctx(mctx), ratio(ratio), blk_bias(blk_bias) {}
477    virtual ~llm_graph_input_qsa() = default;
478
479    void set_input(const llama_ubatch * ubatch) override {
480        mctx->get_idx()->set_input_k_idxs(k_idxs, ubatch);
481        mctx->set_input_qsa(cell_blk, blk_cells, blk_pos, bias, ubatch, ratio, blk_bias);
482    }
483
484    bool can_reuse(const llm_graph_params & params) override {
485        mctx = static_cast<const llama_memory_hybrid_idx_context *>(params.mctx);
486
487        const auto * idx = mctx->get_idx();
488        if (idx == nullptr) {
489            return false;
490        }
491
492        const int64_t n_kv     = idx->get_n_kv();
493        const int64_t n_stream = mctx->get_n_stream();
494        const int64_t n_blocks = (n_kv + ratio - 1)/ratio;
495
496        bool res = true;
497
498        res &= params.ubatch.n_tokens % n_stream == 0;
499
500        res &= k_idxs->ne[0]    == params.ubatch.n_tokens;
501        res &= cell_blk->ne[0]  == n_kv;
502        res &= cell_blk->ne[1]  == n_stream;
503        res &= blk_cells->ne[0] == (int64_t) ratio*n_blocks;
504        res &= blk_pos->ne[0]   == 4*n_blocks*n_stream;
505        res &= bias->ne[0]      == (blk_bias ? n_blocks : n_kv);
506        res &= bias->ne[1]      == params.ubatch.n_tokens/n_stream;
507
508        return res;
509    }
510
511    // per stream: a cell index names a different token in each stream
512    ggml_tensor * k_idxs    = nullptr;   // I32 [n_tokens]
513    ggml_tensor * cell_blk  = nullptr;   // I32 [n_kv, n_stream]
514    ggml_tensor * blk_cells = nullptr;   // I32 [ratio*n_blocks, n_stream]
515    ggml_tensor * blk_pos   = nullptr;   // I32 [4*n_blocks*n_stream]
516    ggml_tensor * bias      = nullptr;   // F32 [n_blocks or n_kv, n_tokens/n_stream, n_stream]
517
518    const llama_memory_hybrid_idx_context * mctx;
519    const uint32_t ratio;
520
521    // the per-cell half of the bias is the attention mask, so only the per-block half is uploaded
522    const bool blk_bias;
523};
524
525ggml_tensor * llama_model_qwen4exp::graph::build_qsa_top_k(
526        const llama_memory_hybrid_idx_context * mctx_hyb,
527        ggml_tensor *                           cur,
528        ggml_tensor *                           inp_pos,
529        ggml_tensor *                           kq_mask,
530        int *                                   sections,
531        int                                     il) {
532    const llama_kv_cache_context * mctx_idx = mctx_hyb->get_idx();
533
534    const int64_t idx_dim  = hparams.indexer_head_size;
535    const int64_t n_idx_h  = hparams.indexer_n_head;
536    const int64_t r        = hparams.dsv4_compress_ratios[il];
537    const int64_t n_kv     = mctx_idx->get_n_kv();
538
539    GGML_ASSERT(r > 0);
540
541    const int64_t n_blocks = (n_kv + r - 1)/r;
542
543    // build_attn_qsa and the KQ mask need the tokens to divide evenly across the streams
544    const int64_t n_stream = mctx_hyb->get_n_stream();
545    GGML_ASSERT(n_tokens % n_stream == 0);
546    const int64_t n_tps = n_tokens/n_stream;
547
548    // only the "which block is visible" half of the bias varies per block
549    // the rest is the visible/not test the attention mask already carries, so upload the per-block half only: 1/ratio of the cells
550    // alibi writes distances instead of a mask and non-causal keeps future cells, so both opt out
551    // the mask also holds an mrope rule for the query's own position, but only 2d image positions can differ there
552    const bool blk_bias = kq_mask != nullptr &&
553        kq_mask->ne[0] == n_kv && kq_mask->ne[1] == n_tps && kq_mask->ne[3] == n_stream &&
554        cparams.causal_attn && !hparams.use_alibi;
555
556    // nothing above depends on the layer, so the layers sharing a ratio share one input set
557    llm_graph_input_qsa * inp = nullptr;
558
559    const auto it = qsa_inps.find((uint32_t) r);
560    if (it != qsa_inps.end()) {
561        inp = it->second;
562    } else {
563        auto qsa = std::make_unique<llm_graph_input_qsa>(mctx_hyb, (uint32_t) r, blk_bias);
564
565        qsa->k_idxs    = mctx_idx->build_input_k_idxs(ctx0, ubatch);
566        qsa->cell_blk  = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, n_kv, n_stream);
567        qsa->blk_cells = ggml_new_tensor_2d(ctx0, GGML_TYPE_I32, r*n_blocks, n_stream);
568        qsa->blk_pos   = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, 4*n_blocks*n_stream);
569        qsa->bias      = ggml_new_tensor_3d(ctx0, GGML_TYPE_F32, blk_bias ? n_blocks : n_kv, n_tps, n_stream);
570
571        ggml_set_input(qsa->cell_blk);
572        ggml_set_input(qsa->blk_cells);
573        ggml_set_input(qsa->blk_pos);
574        ggml_set_input(qsa->bias);
575
576        inp = qsa.get();
577        res->add_input(std::move(qsa));
578        qsa_inps.emplace((uint32_t) r, inp);
579    }
580
581    // cached indexer keys are raw: pooling precedes norm and rotation, so apply neither
582    ggml_tensor * k_raw = build_lora_mm(model.layers[il].index_k_proj, cur);
583    k_raw = ggml_reshape_3d(ctx0, k_raw, idx_dim, 1, n_tokens);
584    cb(k_raw, "indexer_k_raw", il);
585
586    ggml_build_forward_expand(gf, mctx_idx->cpy_k(ctx0, k_raw, inp->k_idxs, il));
587
588    // one key head, so rows are contiguous. get_k gives [idx_dim, n_head_kv, n_kv, n_stream].
589    ggml_tensor * k_all = mctx_idx->get_k(ctx0, il);
590    k_all = ggml_view_3d(ctx0, k_all, idx_dim, n_kv, n_stream, k_all->nb[2], k_all->nb[3], 0);
591
592    // gathers per stream: blk_cells row s indexes stream s's own cells
593    ggml_tensor * members = ggml_get_rows(ctx0, k_all, inp->blk_cells);
594    members = ggml_reshape_4d(ctx0, members, idx_dim, r, n_blocks, n_stream);
595
596    // mean over the block members; r is small, so summing slices beats a transpose plus sum_rows
597    ggml_tensor * pooled = nullptr;
598    for (int64_t i = 0; i < r; ++i) {
599        ggml_tensor * slice = ggml_cont(ctx0,
600                ggml_view_3d(ctx0, members, idx_dim, n_blocks, n_stream,
601                        members->nb[2], members->nb[3], i*members->nb[1]));
602        pooled = pooled ? ggml_add(ctx0, pooled, slice) : slice;
603    }
604    pooled = ggml_scale(ctx0, pooled, 1.0f/(float) r);
605    cb(pooled, "indexer_k_pooled", il);
606
607    // count blocks along ne1: rms_norm launches gridDim.y = ne2, capped at 65535, and 262144/4 = 65536
608    pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, n_blocks*n_stream, 1);
609    pooled = build_norm(pooled, model.layers[il].index_k_norm, nullptr, LLM_NORM_RMS, il);
610
611    // rope wants [n_dims, n_head, n_tokens]: lay every stream's blocks flat, split after.
612    pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, 1, n_blocks*n_stream);
613    pooled = ggml_rope_multi(ctx0, pooled, inp->blk_pos, nullptr,
614            n_rot, sections, rope_type, n_ctx_orig, freq_base, freq_scale,
615            ext_factor, attn_factor, beta_fast, beta_slow);
616    pooled = ggml_reshape_3d(ctx0, pooled, idx_dim, n_blocks, n_stream);
617    cb(pooled, "indexer_k", il);
618
619    ggml_tensor * q = build_lora_mm(model.layers[il].index_q_proj, cur);
620    q = ggml_reshape_3d(ctx0, q, idx_dim, n_idx_h, n_tokens);
621    q = build_norm(q, model.layers[il].index_q_norm, nullptr, LLM_NORM_RMS, il);
622    q = ggml_rope_multi(ctx0, q, inp_pos, nullptr,
623            n_rot, sections, rope_type, n_ctx_orig, freq_base, freq_scale,
624            ext_factor, attn_factor, beta_fast, beta_slow);
625    cb(q, "indexer_q", il);
626
627    // rectify each head dot product before the sum, as in the DeepSeek lightning indexer
628    // mul_mat matches ne[2], so the queries of stream s only meet the blocks of stream s
629    ggml_tensor * score = ggml_mul_mat(ctx0, pooled,
630            ggml_reshape_3d(ctx0, q, idx_dim, n_idx_h*n_tps, n_stream));
631    score = ggml_reshape_4d(ctx0, score, n_blocks, n_idx_h, n_tps, n_stream);
632    score = ggml_relu(ctx0, score);
633
634    // the heads sit side by side on ne[1] and there are only a few of them
635    ggml_tensor * summed = nullptr;
636    for (int64_t h = 0; h < n_idx_h; ++h) {
637        ggml_tensor * slice = ggml_view_3d(ctx0, score, n_blocks, n_tps, n_stream,
638                score->nb[2], score->nb[3], h*score->nb[1]);
639        summed = summed ? ggml_add(ctx0, summed, slice) : ggml_cont(ctx0, slice);
640    }
641
642    score = summed;
643    cb(score, "indexer_score", il);
644
645    // one value per block, so it is cheaper to bias here than after the cells are expanded
646    if (blk_bias) {
647        score = ggml_add(ctx0, score, inp->bias);
648    }
649
650    // every token of a block gets the block score; the budget is whole blocks, so top-k cuts on a block boundary
651    ggml_tensor * expanded = ggml_get_rows(ctx0,
652            ggml_cont(ctx0, ggml_permute(ctx0, score, 1, 0, 2, 3)), inp->cell_blk);
653    expanded = ggml_cont(ctx0, ggml_permute(ctx0, expanded, 1, 0, 2, 3));
654
655    if (blk_bias) {
656        // flash attention keeps the mask in f16; the scores are f32
657        ggml_tensor * mask = kq_mask->type == GGML_TYPE_F32 ? kq_mask : ggml_cast(ctx0, kq_mask, GGML_TYPE_F32);
658        expanded = ggml_add(ctx0, expanded, ggml_reshape_3d(ctx0, mask, n_kv, n_tps, n_stream));
659    } else {
660        expanded = ggml_add(ctx0, expanded, inp->bias);
661    }
662    cb(expanded, "indexer_score_tokens", il);
663
664    // the reference returns indexer_top_k + compress_ratio - 1: whole blocks plus the tail
665    const int64_t width = std::min<int64_t>(n_kv, (int64_t) hparams.indexer_top_k + r - 1);
666
667    ggml_tensor * top_k = ggml_cont(ctx0, ggml_top_k(ctx0, expanded, width));
668
669    // build_attn_qsa reads [n_top_k, n_batch, 1, n_stream], matching the KQ mask.
670    top_k = ggml_reshape_4d(ctx0, top_k, width, n_tps, 1, n_stream);
671    cb(top_k, "indexer_top_k", il);
672
673    return top_k;
674}
675
676// Dense GQA self-attention restricted to the cells that top_k names.
677// The mask build below copies the MLA sparse path in llm_graph_context::build_attn.
678ggml_tensor * llama_model_qwen4exp::graph::build_attn_qsa(
679        llm_graph_input_attn_kv * inp,
680        ggml_tensor *             q_cur,
681        ggml_tensor *             k_cur,
682        ggml_tensor *             v_cur,
683        ggml_tensor *             top_k,
684        float                     kq_scale,
685        int                       il) {
686    // rotate q/k/v before they reach a quantized cache, as the dense path does. the indexer
687    // has already scored with its own query in build_qsa_top_k, so top_k is unaffected.
688    if (inp->self_k_rot) {
689        q_cur = llama_mul_mat_hadamard(ctx0, q_cur, inp->self_k_rot);
690        k_cur = llama_mul_mat_hadamard(ctx0, k_cur, inp->self_k_rot);
691    }
692
693    if (inp->self_v_rot) {
694        v_cur = llama_mul_mat_hadamard(ctx0, v_cur, inp->self_v_rot);
695    }
696
697    // these nodes are added to the graph together so that they are not reordered
698    // by doing so, the number of splits in the graph is reduced
699    // expand k later to enable rope fusion which directly writes into k-v cache
700    ggml_build_forward_expand(gf, q_cur);
701    ggml_build_forward_expand(gf, v_cur);
702    ggml_build_forward_expand(gf, k_cur);
703
704    const auto * mctx_cur = inp->mctx;
705
706    // store to KV cache
707    {
708        const auto & k_idxs = inp->get_k_idxs();
709        const auto & v_idxs = inp->get_v_idxs();
710
711        ggml_build_forward_expand(gf, mctx_cur->cpy_k(ctx0, k_cur, k_idxs, il));
712        ggml_build_forward_expand(gf, mctx_cur->cpy_v(ctx0, v_cur, v_idxs, il));
713    }
714
715    ggml_tensor * kq_mask = inp->get_kq_mask();
716
717    // prepare new kq mask - starts filled with -INFINITY
718    ggml_tensor * kq_mask_all = ggml_fill(ctx0, kq_mask, -INFINITY);
719
720    // reshape KQ mask into tensor with rows of size 1:
721    // [n_kv, n_batch, 1, n_stream] -> [1, n_kv, n_batch, n_stream]
722    kq_mask_all = ggml_view_4d(ctx0, kq_mask_all, 1, kq_mask_all->ne[0], kq_mask_all->ne[1], kq_mask_all->ne[3], kq_mask_all->nb[0], kq_mask_all->nb[1], kq_mask_all->nb[2], 0);
723
724    // reshape top_k indices: [n_top_k, n_batch, 1, n_stream] -> [n_top_k, n_batch, n_stream, 1]
725    ggml_tensor * top_k_3d = ggml_view_4d(ctx0, top_k, top_k->ne[0], top_k->ne[1], top_k->ne[3], 1, top_k->nb[1], top_k->nb[2], top_k->ne[3]*top_k->nb[3], 0);
726
727    // prepare zero-filled tensor with rows of size 1: [1, n_top_k, n_batch, n_stream]
728    // this will be our source of zero values for unmasking top k mask elements
729    ggml_tensor * zeros = ggml_new_tensor_4d(ctx0, GGML_TYPE_F32, 1, top_k_3d->ne[0], top_k_3d->ne[1], top_k_3d->ne[2]);
730    zeros = ggml_fill(ctx0, zeros, 0.0f);
731
732    // modify KQ mask by unmasking elements that are in top_k indices
733    // ggml_set_rows([1, n_kv, n_batch, n_stream], [1, n_top_k, n_batch, n_stream], [n_top_k, n_batch, n_stream, 1])
734    ggml_tensor * kq_mask_top_k = ggml_set_rows(ctx0, kq_mask_all, zeros, top_k_3d);
735
736    // reshape to restore the original shape of KQ mask:
737    // [1, n_kv, n_batch, n_stream] -> [n_kv, n_batch, 1, n_stream]
738    kq_mask_top_k = ggml_view_4d(ctx0, kq_mask_top_k, kq_mask_top_k->ne[1], kq_mask_top_k->ne[2], 1, kq_mask_top_k->ne[3], kq_mask_top_k->nb[2], kq_mask_top_k->nb[3], kq_mask_top_k->nb[3], 0);
739
740    // combine with the original kq mask
741    kq_mask_top_k = ggml_add(ctx0, kq_mask_top_k, kq_mask);
742
743    ggml_tensor * q = q_cur;
744    ggml_tensor * k = mctx_cur->get_k(ctx0, il);
745    ggml_tensor * v = mctx_cur->get_v(ctx0, il);
746
747    // TODO: enable sparse attention when we are ready
748    // ref: https://github.com/ggml-org/llama.cpp/pull/27970
749    //ggml_tensor * cur = build_attn_mha(q, k, v, nullptr, kq_mask_top_k, nullptr, nullptr, top_k->ne[0], kq_scale, il);
750    ggml_tensor * cur = build_attn_mha(q, k, v, nullptr, kq_mask_top_k, nullptr, nullptr, 0, kq_scale, il);
751    cb(cur, "kqv_out", il);
752
753    // the rotation is its own inverse, so undo it on the value side of the output
754    if (inp->self_v_rot) {
755        cur = llama_mul_mat_hadamard(ctx0, cur, inp->self_v_rot);
756    }
757
758    return cur;
759}
760
761ggml_tensor * llama_model_qwen4exp::graph::build_layer_attn(
762        llm_graph_input_attn_kv * inp,
763        const llama_memory_hybrid_idx_context * mctx_hyb,
764        ggml_tensor *             cur,
765        ggml_tensor *             inp_pos,
766        int *                     sections,
767        int                       il) {
768    const int64_t n_embd_head = hparams.n_embd_head_v();
769    GGML_ASSERT(n_embd_head == hparams.n_embd_head_k());
770
771    // indexer reads the same block input as q/k/v; no cache or no ratio means dense
772    const bool qsa = mctx_hyb->get_idx() != nullptr && hparams.dsv4_compress_ratios[il] > 0;
773
774    ggml_tensor * top_k = qsa ? build_qsa_top_k(mctx_hyb, cur, inp_pos, inp->get_kq_mask(), sections, il) : nullptr;
775
776    // Qwen3Next uses a single Q projection that outputs query + gate
777    ggml_tensor * Qcur_full = build_lora_mm(model.layers[il].wq, cur, model.layers[il].wq_s); // [ (n_embd_head * 2) * n_head, n_tokens ]
778    cb(Qcur_full, "Qcur_full", il);
779
780    ggml_tensor * Qcur = ggml_view_3d(ctx0, Qcur_full, n_embd_head, n_head, n_tokens,
781        ggml_element_size(Qcur_full) * n_embd_head * 2,
782        ggml_element_size(Qcur_full) * n_embd_head * 2 * n_head, 0);
783    cb(Qcur, "Qcur_reshaped", il);
784
785    Qcur = build_norm(Qcur, model.layers[il].attn_q_norm, nullptr, LLM_NORM_RMS, il);
786    cb(Qcur, "Qcur_normed", il);
787
788    ggml_tensor * Kcur = build_lora_mm(model.layers[il].wk, cur, model.layers[il].wk_s);
789    cb(Kcur, "Kcur", il);
790
791    ggml_tensor * Vcur = build_lora_mm(model.layers[il].wv, cur, model.layers[il].wv_s);
792    cb(Vcur, "Vcur", il);
793
794    Kcur = ggml_reshape_3d(ctx0, Kcur, n_embd_head, n_head_kv, n_tokens);
795    Kcur = build_norm(Kcur, model.layers[il].attn_k_norm, nullptr, LLM_NORM_RMS, il);
796    cb(Kcur, "Kcur_normed", il);
797
798    ggml_tensor * gate = ggml_view_3d(ctx0, Qcur_full, n_embd_head, n_head, n_tokens,
799        ggml_element_size(Qcur_full) * n_embd_head * 2,
800        ggml_element_size(Qcur_full) * n_embd_head * 2 * n_head,
801        ggml_element_size(Qcur_full) * n_embd_head);
802    gate = ggml_cont_2d(ctx0, gate, n_embd_head * n_head, n_tokens);
803    cb(gate, "gate_reshaped", il);
804
805    Vcur = ggml_reshape_3d(ctx0, Vcur, n_embd_head, n_head_kv, n_tokens);
806
807    // Apply IMRoPE
808    Qcur = ggml_rope_multi(
809            ctx0, Qcur, inp_pos, nullptr,
810            n_rot, sections, rope_type, n_ctx_orig, freq_base, freq_scale,
811            ext_factor, attn_factor, beta_fast, beta_slow
812            );
813
814    Kcur = ggml_rope_multi(
815            ctx0, Kcur, inp_pos, nullptr,
816            n_rot, sections, rope_type, n_ctx_orig, freq_base, freq_scale,
817            ext_factor, attn_factor, beta_fast, beta_slow
818            );
819
820    cb(Qcur, "Qcur", il);
821    cb(Kcur, "Kcur", il);
822    cb(Vcur, "Vcur", il);
823
824    const float kq_scale = hparams.f_attention_scale == 0.0f ? 1.0f / sqrtf(float(n_embd_head)) : hparams.f_attention_scale;
825
826    if (top_k) {
827        cur = build_attn_qsa(inp, Qcur, Kcur, Vcur, top_k, kq_scale, il);
828    } else {
829        cur = build_attn(inp,
830                    nullptr, nullptr, nullptr,
831                    Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il);
832    }
833    cb(cur, "attn_pregate", il);
834
835    ggml_tensor * gate_sigmoid = ggml_sigmoid(ctx0, gate);
836    cb(gate_sigmoid, "gate_sigmoid", il);
837
838    cur = ggml_mul(ctx0, cur, gate_sigmoid);
839    cb(cur, "attn_gated", il);
840
841    cur = build_lora_mm(model.layers[il].wo, cur, model.layers[il].wo_s);
842    cb(cur, "attn_output", il);
843
844    return cur;
845}
846
847ggml_tensor * llama_model_qwen4exp::graph::build_layer_attn_linear(
848        llm_graph_input_rs * inp,
849        ggml_tensor *        cur,
850        int                  il) {
851    const auto * mctx_cur = inp->mctx;
852
853    const int64_t d_inner      = hparams.ssm_d_inner;
854    const int64_t n_seqs       = ubatch.n_seqs;
855    const int64_t head_k_dim   = hparams.ssm_d_state;
856    const int64_t num_k_heads  = hparams.ssm_n_group;
857    const int64_t num_v_heads  = hparams.ssm_dt_rank;
858    const int64_t head_v_dim   = hparams.ssm_d_state;
859    const int64_t n_seq_tokens = ubatch.n_seq_tokens;
860
861    GGML_ASSERT(n_seqs != 0);
862    GGML_ASSERT(ubatch.equal_seqs());
863    GGML_ASSERT(ubatch.n_tokens == n_seq_tokens * n_seqs);
864    GGML_ASSERT(head_v_dim * num_v_heads == d_inner);
865
866    auto qkvz = build_qkvz(cur, il);
867    ggml_tensor * qkv_mixed = qkvz.first;
868    ggml_tensor * z         = qkvz.second;
869
870    ggml_tensor * beta = build_lora_mm(model.layers[il].ssm_beta, cur, model.layers[il].ssm_beta_s);
871    beta = ggml_reshape_4d(ctx0, beta, 1, num_v_heads, n_seq_tokens, n_seqs);
872    cb(beta, "beta", il);
873
874    beta = ggml_sigmoid(ctx0, beta);
875    cb(beta, "beta_sigmoid", il);
876
877    ggml_tensor * alpha = build_lora_mm(model.layers[il].ssm_alpha, cur, model.layers[il].ssm_alpha_s);
878    alpha = ggml_reshape_3d(ctx0, alpha, num_v_heads, n_seq_tokens, n_seqs);
879    cb(alpha, "alpha", il);
880
881    ggml_tensor * alpha_biased   = ggml_add(ctx0, alpha, model.layers[il].ssm_dt);
882    ggml_tensor * alpha_softplus = ggml_softplus(ctx0, alpha_biased);
883    cb(alpha_softplus, "a_softplus", il);
884
885    ggml_tensor * gate = ggml_mul(ctx0, alpha_softplus, model.layers[il].ssm_a);  // -A_log.exp() * softplus
886    cb(gate, "gate", il);
887
888    gate = ggml_reshape_4d(ctx0, gate, 1, num_v_heads, n_seq_tokens, n_seqs);
889
890    ggml_tensor * conv_states_all = mctx_cur->get_r_l(il);
891    ggml_tensor * ssm_states_all  = mctx_cur->get_s_l(il);
892
893    ggml_tensor * conv_kernel      = model.layers[il].ssm_conv1d;
894    const int64_t conv_kernel_size = conv_kernel->ne[0];
895
896    // the channels must match how load_arch_tensors sizes wqkv, not ssm_d_inner
897    const int64_t conv_channels    = head_k_dim * num_k_heads * 2 + head_v_dim * num_v_heads;
898
899    ggml_tensor * conv_input = build_conv_state_at(inp, conv_states_all, qkv_mixed,
900            conv_kernel_size - 1, conv_channels, il);
901
902    ggml_tensor * state = build_rs(inp, ssm_states_all, hparams.n_embd_s(), n_seqs);
903    state = ggml_reshape_4d(ctx0, state, head_v_dim, head_v_dim, num_v_heads, n_seqs);
904    cb(state, "state_predelta", il);
905
906    ggml_tensor * conv_output_proper = ggml_ssm_conv(ctx0, conv_input, conv_kernel);
907    cb(conv_output_proper, "conv_output_raw", il);
908
909    ggml_tensor * conv_output_silu = ggml_silu(ctx0, conv_output_proper);
910    cb(conv_output_silu, "conv_output_silu", il);
911
912    ggml_tensor * conv_qkv_mix = conv_output_silu;
913
914    int64_t nb1_qkv = ggml_row_size(conv_qkv_mix->type, conv_channels);
915
916    // Extract the convolved Q, K, V from conv_output
917    ggml_tensor * q_conv = ggml_view_4d(ctx0, conv_qkv_mix, head_k_dim, num_k_heads, n_seq_tokens, n_seqs,
918            ggml_row_size(conv_qkv_mix->type, head_k_dim),
919            nb1_qkv,
920            nb1_qkv * n_seq_tokens,
921            0);
922
923    ggml_tensor * k_conv = ggml_view_4d(ctx0, conv_qkv_mix, head_k_dim, num_k_heads, n_seq_tokens, n_seqs,
924            ggml_row_size(conv_qkv_mix->type, head_k_dim),
925            nb1_qkv,
926            nb1_qkv * n_seq_tokens,
927            head_k_dim * num_k_heads * ggml_element_size(conv_qkv_mix));
928
929    ggml_tensor * v_conv = ggml_view_4d(ctx0, conv_qkv_mix, head_v_dim, num_v_heads, n_seq_tokens, n_seqs,
930            ggml_row_size(conv_qkv_mix->type, head_v_dim),
931            nb1_qkv,
932            nb1_qkv * n_seq_tokens,
933            ggml_row_size(conv_qkv_mix->type, 2 * head_k_dim * num_k_heads));
934
935    cb(q_conv, "q_conv", il);
936    cb(k_conv, "k_conv", il);
937    cb(v_conv, "v_conv", il);
938
939
940    const float eps_norm = hparams.f_norm_rms_eps;
941
942    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
943    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);
944
945    // repeat to match shapes when head keys != value keys; unneeded with the fused GDN
946    if (num_k_heads != num_v_heads && (!cparams.fused_gdn_ar || !cparams.fused_gdn_ch)) {
947        GGML_ASSERT(num_v_heads % num_k_heads == 0);
948        q_conv = ggml_repeat_4d(ctx0, q_conv, head_k_dim, num_v_heads, n_seq_tokens, n_seqs);
949        k_conv = ggml_repeat_4d(ctx0, k_conv, head_k_dim, num_v_heads, n_seq_tokens, n_seqs);
950    }
951
952    cb(q_conv, "q_conv_predelta", il);
953    cb(k_conv, "k_conv_predelta", il);
954    cb(v_conv, "v_conv_predelta", il);
955
956    ggml_tensor * output = build_recurrent_attn(inp, ssm_states_all, q_conv, k_conv, v_conv, gate, beta, state, il);
957
958    ggml_tensor * z_2d = ggml_reshape_4d(ctx0, z, head_v_dim, num_v_heads, n_seq_tokens, n_seqs);
959
960    // gated normalization, as self.norm(core_attn_out, z) in the reference
961    ggml_tensor * attn_out_norm = build_norm_gated(output, model.layers[il].ssm_norm, z_2d, il);
962
963    ggml_tensor * final_output = ggml_reshape_3d(ctx0, attn_out_norm, head_v_dim * num_v_heads, n_seq_tokens, n_seqs);
964    cb(final_output, "final_output", il);
965
966    cur = build_lora_mm(model.layers[il].ssm_out, final_output, model.layers[il].ssm_out_s);
967    cb(cur, "linear_attn_out", il);
968
969    cur = ggml_reshape_2d(ctx0, cur, n_embd, n_seq_tokens * n_seqs);
970
971    return cur;
972}
973
974ggml_tensor * llama_model_qwen4exp::graph::build_layer_ffn(ggml_tensor * cur, const int il) {
975    GGML_ASSERT(model.layers[il].ffn_gate_inp != nullptr);
976
977    ggml_tensor * moe_out =
978        build_moe_ffn(cur,
979            model.layers[il].ffn_gate_inp,
980            model.layers[il].ffn_up_exps,
981            model.layers[il].ffn_gate_exps,
982            model.layers[il].ffn_down_exps,
983            nullptr,
984            n_expert, n_expert_used,
985            LLM_FFN_SILU, true,
986            hparams.expert_weights_scale,
987            LLAMA_EXPERT_GATING_FUNC_TYPE_SOFTMAX, il,
988            nullptr, model.layers[il].ffn_gate_up_exps,
989            model.layers[il].ffn_up_exps_s,
990            model.layers[il].ffn_gate_exps_s,
991            model.layers[il].ffn_down_exps_s);
992    cb(moe_out, "ffn_moe_out", il);
993
994    // shared experts, as in the Qwen3Next reference
995    if (model.layers[il].ffn_up_shexp != nullptr) {
996        ggml_tensor * ffn_shexp =
997            build_ffn(cur,
998                model.layers[il].ffn_up_shexp, NULL, model.layers[il].ffn_up_shexp_s,
999                model.layers[il].ffn_gate_shexp, NULL, model.layers[il].ffn_gate_shexp_s,
1000                model.layers[il].ffn_down_shexp, NULL, model.layers[il].ffn_down_shexp_s,
1001                NULL,
1002                LLM_FFN_SILU, LLM_FFN_PAR, il);
1003        cb(ffn_shexp, "ffn_shexp", il);
1004
1005        // shared expert has its own sigmoided gate (ffn_gate_inp_shexp, one value per token)
1006        ggml_tensor * shared_gate = build_lora_mm(model.layers[il].ffn_gate_inp_shexp, cur);
1007        cb(shared_gate, "shared_expert_gate", il);
1008
1009        shared_gate = ggml_sigmoid(ctx0, shared_gate);
1010        cb(shared_gate, "shared_expert_gate_sigmoid", il);
1011
1012        ffn_shexp = ggml_mul(ctx0, ffn_shexp, shared_gate);
1013        cb(ffn_shexp, "ffn_shexp_gated", il);
1014
1015        cur = ggml_add(ctx0, moe_out, ffn_shexp);
1016        cb(cur, "ffn_out", il);
1017    } else {
1018        cur = moe_out;
1019    }
1020
1021    return cur;
1022}
1023
1024// PLE n-gram hash embedding: each token gathers ple_n_heads rows of a shared table.
1025//   mixed_n = (t[p]*m[0]) ^ ... ^ (t[p-n+1]*m[n-1]);  row = mixed_n % vocab[h] + offset[h]
1026// The hash runs host-side because ggml has no int64 and no xor. EOS resets the window.
1027
1028class llm_graph_input_ple : public llm_graph_input_i {
1029public:
1030    llm_graph_input_ple(const llama_model_qwen4exp & pmodel,
1031                        const llama_kv_cache_context * mctx) : pmodel(pmodel), mctx(mctx) {}
1032    virtual ~llm_graph_input_ple() = default;
1033
1034    void set_input(const llama_ubatch * ubatch) override;
1035
1036    bool can_reuse(const llm_graph_params & params) override {
1037        mctx = static_cast<const llama_memory_hybrid_idx_context *>(params.mctx)->get_attn();
1038        return rows->ne[0] == (int64_t) pmodel.hparams.ple_n_heads * params.ubatch.n_tokens;
1039    }
1040
1041    ggml_tensor * rows = nullptr;   // I32 [ple_n_heads * n_tokens]
1042
1043    const llama_model_qwen4exp & pmodel;
1044
1045    // the predecessor tokens live in the attention KV cells (ext.tok)
1046    const llama_kv_cache_context * mctx;
1047
1048    // scratch, reused across set_input() calls
1049    std::vector<llama_token> prev;
1050};
1051
1052void llm_graph_input_ple::set_input(const llama_ubatch * ubatch) {
1053    const auto & hp = pmodel.hparams;
1054
1055    // an image arrives as an embd batch, so ubatch->token is null, but every position still needs a row for ggml_get_rows
1056    // stand in the image token id that the reference hashes, or EOS if the file has no such key
1057    // gemma3n and gemma4 do the same with a hardcoded row 0 of per_layer_token_embd.
1058    const llama_token img_tok = hp.ple_image_token_id != 0
1059        ? (llama_token) hp.ple_image_token_id
1060        : (llama_token) hp.ple_eos_token_id;
1061    auto tok_of = [&](int64_t k) -> llama_token {
1062        return ubatch->token ? ubatch->token[k] : img_tok;
1063    };
1064
1065    const int64_t n_tokens = ubatch->n_tokens;
1066    const int64_t n_gram   = hp.ple_ngram_size;
1067    const int64_t n_heads  = hp.ple_n_heads;
1068    const int64_t per_gram = hp.ple_heads_per_ngram;
1069    const int64_t eos      = hp.ple_eos_token_id;
1070    const int64_t n_prev   = n_gram - 1;
1071
1072    std::vector<int32_t> idx(n_heads * n_tokens);
1073
1074    GGML_ASSERT(mctx != nullptr);
1075
1076    for (int64_t i = 0; i < n_tokens; ++i) {
1077        // the preceding tokens would be ambiguous, see get_prev_tokens()
1078        GGML_ASSERT(ubatch->n_seq_id[i] == 1 && "PLE n-gram embeddings do not support tokens shared by multiple sequences");
1079    }
1080
1081    // predecessors come from the KV cells (ext.tok); apply_ubatch() already stored this ubatch, so its own tokens count too
1082    mctx->get_prev_tokens(*ubatch, n_prev, prev);
1083
1084    for (int64_t i = 0; i < n_tokens; ++i) {
1085        // an EOS in the window resets everything at or before it
1086        // a missing predecessor (before the sequence start, or no cached cell) reads as EOS
1087        // the EOS of the token itself does not cut its own context, as in the reference
1088        std::vector<int64_t> ctx(n_gram);
1089        ctx[0] = tok_of(i);
1090        bool cut = false;
1091        for (int64_t s = 1; s < n_gram; ++s) {
1092            // predecessor s positions back; prev[] is oldest-first, missing entries are LLAMA_TOKEN_NULL
1093            const llama_token t = cut ? LLAMA_TOKEN_NULL : prev[i*n_prev + (n_prev - s)];
1094            cut = cut || t < 0 || t == eos;
1095            ctx[s] = cut ? eos : t;
1096        }
1097
1098        for (int64_t n = 2; n <= n_gram; ++n) {
1099            uint64_t mixed = (uint64_t) ctx[0] * hp.ple_layer_multipliers[0];
1100            for (int64_t j = 1; j < n; ++j) {
1101                mixed ^= (uint64_t) ctx[j] * hp.ple_layer_multipliers[j];
1102            }
1103            const int64_t base = (n - 2) * per_gram;
1104            for (int64_t g = 0; g < per_gram; ++g) {
1105                const int64_t h_i = base + g;
1106                idx[i * n_heads + h_i] =
1107                    (int32_t) (mixed % hp.ple_head_vocab_sizes[h_i] + hp.ple_head_offsets[h_i]);
1108            }
1109        }
1110    }
1111
1112    ggml_backend_tensor_set(rows, idx.data(), 0, idx.size()*ggml_element_size(rows));
1113}
1114
1115// Read a conv history out of its own recurrent row and write the new tail back.
1116// The shared build_conv_state cannot do this: qwen4exp has two such rows per layer.
1117ggml_tensor * llama_model_qwen4exp::graph::build_conv_state_at(
1118        llm_graph_input_rs * inp,
1119        ggml_tensor *        conv_states_all,
1120        ggml_tensor *        x,
1121        int64_t              state_cols,
1122        int64_t              channels,
1123        int                  il) {
1124    const auto * mctx_cur = inp->mctx;
1125
1126    const auto kv_head = mctx_cur->get_head();
1127
1128    const int64_t n_seqs    = ubatch.n_seqs;
1129    const int64_t row_total = conv_states_all->ne[0];
1130
1131    // the row is exactly this convolution's state, so the gather is reused as a whole
1132    GGML_ASSERT(state_cols * channels == row_total);
1133
1134    auto it = rs_rows.find(conv_states_all);
1135    if (it == rs_rows.end()) {
1136        it = rs_rows.emplace(conv_states_all, build_rs(inp, conv_states_all, row_total, n_seqs)).first;
1137    }
1138    ggml_tensor * rows = it->second;
1139
1140    ggml_tensor * state = ggml_reshape_3d(ctx0, rows, state_cols, channels, n_seqs);
1141    cb(state, "conv_state_at", il);
1142
1143    ggml_tensor * conv_input = ggml_concat(ctx0, state, ggml_transpose(ctx0, x), 0);
1144
1145    // [TAG_RECURRENT_ROLLBACK_SPLITS] keep the last state_cols columns once per rollback slot,
1146    // slot s ending s tokens earlier so a rollback of s tokens reads a history that never saw them
1147    const size_t row_size = ggml_row_size(conv_states_all->type, row_total);
1148    const uint32_t mem_size = mctx_cur->get_size();
1149
1150    const int64_t n_slots = (int64_t) cparams.n_rs_seq + 1;
1151
1152    for (int64_t slot = 0; slot < n_slots; ++slot) {
1153        const int64_t s_idx = std::max<int64_t>(0, conv_input->ne[0] - state_cols - slot);
1154
1155        ggml_tensor * tail = ggml_view_3d(ctx0, conv_input,
1156                state_cols, channels, n_seqs,
1157                conv_input->nb[1], conv_input->nb[2],
1158                ggml_row_size(conv_input->type, s_idx));
1159
1160        ggml_tensor * dst = ggml_view_2d(ctx0, conv_states_all,
1161                state_cols * channels, n_seqs,
1162                conv_states_all->nb[1],
1163                (slot * mem_size + kv_head) * row_size);
1164
1165        ggml_build_forward_expand(gf, ggml_cpy(ctx0, ggml_cont(ctx0, tail), dst));
1166    }
1167
1168    return conv_input;
1169}
1170
1171ggml_tensor * llama_model_qwen4exp::graph::build_inp_ple(
1172        const llama_memory_hybrid_idx_context * mctx_hyb) {
1173    const int64_t n_heads = hparams.ple_n_heads;
1174
1175    // the attention cells see every ubatch regardless of the layer types
1176    auto ple_inp = std::make_unique<llm_graph_input_ple>(
1177            static_cast<const llama_model_qwen4exp &>(model), mctx_hyb->get_attn());
1178
1179    ple_inp->rows = ggml_new_tensor_1d(ctx0, GGML_TYPE_I32, n_heads * n_tokens);
1180    ggml_set_input(ple_inp->rows);
1181    ggml_tensor * rows = ple_inp->rows;
1182    res->add_input(std::move(ple_inp));
1183
1184    // gather then flatten the heads: get_rows lays the head dimension out slowest, as the reference does
1185    ggml_tensor * emb = ggml_get_rows(ctx0, model.per_layer_tok_embd, rows);
1186    emb = ggml_reshape_2d(ctx0, emb, hparams.ple_head_dim * n_heads, n_tokens);
1187    cb(emb, "ple_embd", -1);
1188
1189    return emb;
1190}
1191
1192ggml_tensor * llama_model_qwen4exp::graph::build_ple(
1193        llm_graph_input_rs * inp,
1194        ggml_tensor *        emb,
1195        ggml_tensor *        hidden,
1196        int                  il) {
1197    const int64_t hc      = hparams.dsv4_hc_mult;
1198    const int64_t hc_dim  = hc * n_embd;
1199
1200    ggml_tensor * key   = build_lora_mm(model.layers[il].ple_key,   emb);
1201    ggml_tensor * value = build_lora_mm(model.layers[il].ple_value, emb);
1202
1203    // both norms group over one hc stream, with a [n_embd, hc] weight
1204    auto grouped_norm = [&](ggml_tensor * x, ggml_tensor * w) {
1205        ggml_tensor * t = ggml_reshape_3d(ctx0, x, n_embd, hc, n_tokens);
1206        return ggml_mul(ctx0, ggml_rms_norm(ctx0, t, hparams.f_norm_rms_eps), w);
1207    };
1208
1209    key = grouped_norm(key, model.layers[il].ple_norm_key);
1210    ggml_tensor * query = grouped_norm(hidden, model.layers[il].ple_norm_query);
1211
1212    // per-stream dot product, then a signed square root before the sigmoid
1213    ggml_tensor * s = ggml_sum_rows(ctx0, ggml_mul(ctx0, key, query));
1214    s = ggml_scale(ctx0, s, 1.0f / sqrtf((float) n_embd));
1215
1216    ggml_tensor * mag  = ggml_sqrt(ctx0, ggml_clamp(ctx0, ggml_abs(ctx0, s), 1e-6f, INFINITY));
1217    ggml_tensor * gate = ggml_sigmoid(ctx0, ggml_mul(ctx0, ggml_sgn(ctx0, s), mag));
1218    cb(gate, "ple_gate", il);
1219
1220    // [n_embd, 1, T] value broadcast across the hc streams, scaled by the gate
1221    ggml_tensor * v3 = ggml_reshape_3d(ctx0, value, n_embd, 1, n_tokens);
1222    v3 = ggml_repeat_4d(ctx0, v3, n_embd, hc, n_tokens, 1);
1223
1224    ggml_tensor * gated = ggml_mul(ctx0, v3, gate);
1225    cb(gated, "ple_gated_value", il);
1226
1227    ggml_tensor * normalized = grouped_norm(
1228            ggml_reshape_2d(ctx0, gated, hc_dim, n_tokens),
1229            model.layers[il].ple_norm_conv);
1230    normalized = ggml_reshape_2d(ctx0, normalized, hc_dim, n_tokens);
1231
1232    // depthwise causal conv, dilated by the n-gram size, as a sum of shifted copies
1233    // ggml_conv_1d_dw is documented as unreliable:
1234    //   out[c, t] = sum_k w[k, c] * x[c, t - (K-1-k)*dilation]
1235    // The history of the earlier ubatches is prepended, so a chunked prefill matches a single-shot one.
1236    const int64_t kern = hparams.ple_conv_kernel;
1237    const int64_t dil  = hparams.ple_ngram_size;
1238    const int64_t hist = (kern - 1) * dil;
1239
1240    // the conv history is per sequence, so the input carries the sequence axis too
1241    const int64_t n_seqs       = ubatch.n_seqs;
1242    const int64_t n_seq_tokens = ubatch.n_seq_tokens;
1243
1244    // [hist + n_seq_tokens, hc_dim, n_seqs], tokens on ne[0]
1245    ggml_tensor * padded = build_conv_state_at(inp, inp->mctx->get_p_l(il),
1246            ggml_reshape_3d(ctx0, normalized, hc_dim, n_seq_tokens, n_seqs),
1247            hist, hc_dim, il);
1248
1249    ggml_tensor * conv_out = nullptr;
1250    for (int64_t k = 0; k < kern; ++k) {
1251        // tap k reads (kern-1-k)*dilation positions back
1252        const int64_t start = hist - (kern - 1 - k) * dil;
1253
1254        ggml_tensor * shifted = ggml_cont(ctx0,
1255                ggml_transpose(ctx0,
1256                        ggml_view_3d(ctx0, padded, n_seq_tokens, hc_dim, n_seqs,
1257                                padded->nb[1], padded->nb[2],
1258                                ggml_row_size(padded->type, start))));
1259
1260        // column k of the [kern, hc_dim] kernel is one weight per channel
1261        ggml_tensor * wk = ggml_cont(ctx0,
1262                ggml_view_2d(ctx0, model.layers[il].ple_conv1d, 1, hc_dim,
1263                        model.layers[il].ple_conv1d->nb[1],
1264                        k * model.layers[il].ple_conv1d->nb[0]));
1265        // this kernel keeps the file type, so cast it before it multiplies an f32 activation
1266        wk = ggml_reshape_1d(ctx0, wk, hc_dim);
1267        if (wk->type != GGML_TYPE_F32) {
1268            wk = ggml_cast(ctx0, wk, GGML_TYPE_F32);
1269        }
1270
1271        ggml_tensor * term = ggml_mul(ctx0, shifted, wk);
1272        conv_out = conv_out ? ggml_add(ctx0, conv_out, term) : term;
1273    }
1274
1275    conv_out = ggml_silu(ctx0, conv_out);
1276    conv_out = ggml_reshape_3d(ctx0, ggml_cont(ctx0, conv_out), n_embd, hc, n_tokens);
1277    cb(conv_out, "ple_conv_out", il);
1278
1279    return ggml_add(ctx0, hidden, ggml_add(ctx0, gated, conv_out));
1280}