Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions driver/portable/src/gguf_archive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,30 @@ std::string gguf_to_hf_name(const std::string& g) {
if (suffix == "ffn_gate_exps.weight") return layer + "mlp.experts.gate_proj.weight";
if (suffix == "ffn_up_exps.weight") return layer + "mlp.experts.up_proj.weight";
if (suffix == "ffn_down_exps.weight") return layer + "mlp.experts.down_proj.weight";
// Qwen 3.5 / 3.6 shared expert (qwen35moe). `ffn_gate_inp_shexp` is the
// 1-D sigmoid gate over the shared expert (HF `mlp.shared_expert_gate`),
// distinct from the per-token router `ffn_gate_inp`.
if (suffix == "ffn_gate_shexp.weight") return layer + "mlp.shared_expert.gate_proj.weight";
if (suffix == "ffn_up_shexp.weight") return layer + "mlp.shared_expert.up_proj.weight";
if (suffix == "ffn_down_shexp.weight") return layer + "mlp.shared_expert.down_proj.weight";
if (suffix == "ffn_gate_inp_shexp.weight") return layer + "mlp.shared_expert_gate.weight";
// Qwen 3.5 / 3.6 hybrid gated-delta-rule linear attention. The GGUF
// (llama.cpp qwen35 convention) packs the linear-attn input projection
// as `attn_qkv` (the fused q/k/v of the delta net) + `attn_gate` (the
// output Z gate), and the gated-delta params as `ssm_*`. These names
// only ever appear on the *linear* layers; full-attention layers carry
// the ordinary `attn_q/k/v` handled above. The convert-time value
// transforms (A_log = -exp(raw); folded norm.weight +1) are inverted at
// load in `model.cpp::build_qwen3_5_`, not here.
if (suffix == "attn_qkv.weight") return layer + "linear_attn.in_proj_qkv.weight";
if (suffix == "attn_gate.weight") return layer + "linear_attn.in_proj_z.weight";
if (suffix == "ssm_alpha.weight") return layer + "linear_attn.in_proj_a.weight";
if (suffix == "ssm_beta.weight") return layer + "linear_attn.in_proj_b.weight";
if (suffix == "ssm_a") return layer + "linear_attn.A_log";
if (suffix == "ssm_dt.bias") return layer + "linear_attn.dt_bias";
if (suffix == "ssm_conv1d.weight") return layer + "linear_attn.conv1d.weight";
if (suffix == "ssm_norm.weight") return layer + "linear_attn.norm.weight";
if (suffix == "ssm_out.weight") return layer + "linear_attn.out_proj.weight";
return {};
}

Expand Down
1 change: 1 addition & 0 deletions driver/portable/src/gguf_archive.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class GGUFArchive : public WeightArchive {
const StTensor* find(const std::string& name) const noexcept override;
const StTensor& at(const std::string& name) const override;
std::size_t num_tensors() const noexcept override { return tensors_.size(); }
bool is_gguf() const noexcept override { return true; }

const GgufMeta& meta() const noexcept { return meta_; }

Expand Down
59 changes: 59 additions & 0 deletions driver/portable/src/gguf_hparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,65 @@ Hparams parse_gguf_hparams(const GgufMeta& meta) {
h.sliding_window = v;
}

// ---- Qwen 3.5 / 3.6 hybrid (gated-delta-rule linear attn + GQA) -----
// The safetensors path fills these from config.json (hf_config.cpp);
// for a GGUF the same facts live under the `qwen35.*` kv namespace.
// Without them `model.cpp::build_qwen3_5_` throws "layer_types missing".
if (h.arch == PieArch::Qwen3_5) {
// Linear-attention dims. The GGUF `ssm.*` keys carry the same
// quantities the HF config exposes as `linear_*`:
// group_count -> num K heads (ssm_n_group)
// time_step_rank -> num V heads (ssm_dt_rank)
// state_size -> K and V head dim (ssm_d_state)
// conv_kernel -> depthwise conv width
// Verified against real blobs by the identity
// ssm.inner_size == num_v_heads * v_head_dim
// (0.8B: 16*128=2048; 27B: 48*128=6144).
h.qwen35_linear_num_k_heads =
static_cast<std::int32_t>(get_num(meta, a, "ssm.group_count", 0));
h.qwen35_linear_num_v_heads =
static_cast<std::int32_t>(get_num(meta, a, "ssm.time_step_rank", 0));
h.qwen35_linear_k_head_dim =
static_cast<std::int32_t>(get_num(meta, a, "ssm.state_size", 0));
h.qwen35_linear_v_head_dim = h.qwen35_linear_k_head_dim;
h.qwen35_linear_conv_kernel =
static_cast<std::int32_t>(get_num(meta, a, "ssm.conv_kernel", 4));
// A GGUF always carries the converter's tiled V-head order; the
// graph must expand Q/K with a tiled broadcast to match.
h.qwen35_linear_v_tiled = true;

// qwen35moe shared expert (always-active dense path alongside the
// routed experts). The generic MoE block above already read
// expert_count / expert_used_count / expert_feed_forward_length.
h.shared_expert_intermediate_size = static_cast<std::int32_t>(
get_num(meta, a, "expert_shared_feed_forward_length", 0));

// Per-layer attention type. The GGUF stores only the stride
// `full_attention_interval`; llama.cpp derives the pattern as
// "layer i is full attention iff (i+1) % interval == 0, else
// linear" (every interval-th layer, last of each group). pie
// encodes full='g', linear='l' in `layer_types`.
const std::int32_t interval = static_cast<std::int32_t>(
get_num(meta, a, "full_attention_interval", 4));
h.qwen35_full_attn_interval = interval;
if (interval > 0 && h.num_hidden_layers > 0) {
h.layer_types.assign(static_cast<std::size_t>(h.num_hidden_layers), 'l');
for (std::int32_t i = 0; i < h.num_hidden_layers; ++i) {
if (((i + 1) % interval) == 0) {
h.layer_types[static_cast<std::size_t>(i)] = 'g';
}
}
}

// Qwen 3.5 / 3.6 full-attention layers always carry an output gate
// FUSED into a double-wide `q_proj` (out = 2 * n_heads * head_dim:
// half query, half gate), so there is no separate gate tensor in
// the GGUF. `graph_qwen3_5.cpp` splits it when this flag is set;
// leaving it false reshapes the double-wide Q to the single-width
// head layout and aborts (ggml_nelements mismatch).
h.qwen35_attn_output_gate = true;
}

return h;
}

Expand Down
25 changes: 17 additions & 8 deletions driver/portable/src/graph_qwen3_5.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,27 @@ ggml_tensor* build_qwen3_5_linear_layer(
auto* k = ggml_reshape_4d(ctx, k_flat, S, n_kh, n_tokens, n_seqs);
auto* v = ggml_reshape_4d(ctx, v_flat, S, n_vh, n_tokens, n_seqs);

// ---- 3a. GQA: repeat_interleave Q/K along the head axis to match V.
// Q' [s, k*f+i, t, b] = Q[s, k, t, b] for all i in [0, f).
// Implemented as reshape→repeat_4d (tile along inserted dim)→reshape.
// ---- 3a. GQA: expand Q/K along the head axis to match V's n_vh heads.
// The expansion order must match how V's heads are laid out:
// · grouped (HF safetensors): Q'[s, k*f+i] = Q[s, k] — repeat_interleave
// · tiled (GGUF converter): Q'[s, k + n_kh*i] = Q[s, k] — ggml_repeat
// Both via reshape→repeat_4d→reshape; only the inserted-dim position
// differs (before vs after the head axis).
if (gqa_f > 1) {
auto interleave = [&](ggml_tensor* x) {
x = ggml_reshape_4d(ctx, x, S, 1, n_kh, n_tokens * n_seqs);
x = ggml_repeat_4d(ctx, x, S, gqa_f, n_kh, n_tokens * n_seqs);
const bool tiled = h.qwen35_linear_v_tiled;
auto expand = [&](ggml_tensor* x) {
if (tiled) {
x = ggml_reshape_4d(ctx, x, S, n_kh, 1, n_tokens * n_seqs);
x = ggml_repeat_4d(ctx, x, S, n_kh, gqa_f, n_tokens * n_seqs);
} else {
x = ggml_reshape_4d(ctx, x, S, 1, n_kh, n_tokens * n_seqs);
x = ggml_repeat_4d(ctx, x, S, gqa_f, n_kh, n_tokens * n_seqs);
}
x = ggml_cont(ctx, x);
return ggml_reshape_4d(ctx, x, S, H, n_tokens, n_seqs);
};
q = interleave(q);
k = interleave(k);
q = expand(q);
k = expand(k);
}

// ---- 4. L2-norm. The fused op applies the 1/sqrt(S) Q-scale itself
Expand Down
7 changes: 7 additions & 0 deletions driver/portable/src/hf_config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ struct Hparams {
// mrope + output gate. Vision tower + multi-token-prediction head
// are present in checkpoints but ignored by the driver.
bool qwen35_attn_output_gate = false;
// GGUF stores the linear-attention V heads in *tiled* order (the
// llama.cpp qwen35 converter permutes grouped->tiled when
// num_v_heads != num_k_heads so ggml's tiled broadcast aligns); HF
// safetensors keep the original grouped order. The graph expands Q/K
// to match V using a tiled `ggml_repeat` when set, else a grouped
// `repeat_interleave`.
bool qwen35_linear_v_tiled = false;
std::int32_t qwen35_full_attn_interval = 0;
std::int32_t qwen35_linear_num_k_heads = 0;
std::int32_t qwen35_linear_num_v_heads = 0;
Expand Down
Loading
Loading