Skip to content

Fix/qwen35 gguf loading#466

Merged
ingim merged 2 commits into
mainfrom
fix/qwen35-gguf-loading
Jul 11, 2026
Merged

Fix/qwen35 gguf loading#466
ingim merged 2 commits into
mainfrom
fix/qwen35-gguf-loading

Conversation

@zyma98

@zyma98 zyma98 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Fix loading errors when loading Qwen 3.5/3.6 GGUF.

Summary by CodeRabbit

  • New Features

    • Added support for loading Qwen 3.5/3.6 hybrid and shared-expert model weights from GGUF files.
    • Improved compatibility with multimodal-wrapped model checkpoints.
    • Added support for GGUF-specific tensor layouts, normalization formats, and linear-attention parameters.
  • Bug Fixes

    • Corrected Qwen linear-attention head expansion for tiled and grouped layouts.
    • Improved handling of small model weights across GGUF and Safetensors formats.

shsym added 2 commits July 10, 2026 16:50
The portable driver could parse a qwen35/qwen35moe GGUF's architecture but
not actually load it: `build_qwen3_5_` threw "layer_types missing", and even
past that the GGUF→HF tensor mapper had no linear-attn / shared-expert cases
and did not invert the converter's value transforms. Only the safetensors
path was ever exercised. This teaches the GGUF path the full hybrid arch.

gguf_hparams.cpp: derive `layer_types` from `qwen35.full_attention_interval`
(layer i is full attention iff (i+1) % interval == 0, else linear), and fill
the linear-attention dims from the `qwen35.ssm.*` keys (group_count→num K
heads, time_step_rank→num V heads, state_size→head dim, conv_kernel). Enable
`qwen35_attn_output_gate` — Qwen 3.5/3.6 fuse the attention output gate into a
double-wide q_proj, which the graph splits.

gguf_archive.cpp: map the qwen35 GGUF tensor names — `attn_qkv`/`attn_gate`
→ linear_attn.in_proj_qkv/in_proj_z, `ssm_alpha`/`ssm_beta` → in_proj_a/b,
`ssm_a`/`ssm_dt.bias`/`ssm_conv1d`/`ssm_norm`/`ssm_out` → A_log/dt_bias/conv1d
/norm/out_proj, and the `*_shexp` shared-expert tensors.

model.cpp: invert the llama.cpp converter's value transforms at load for a
GGUF source (`A_log` stored as `-exp`, `*norm.weight` folded `+1` except
`linear_attn.norm`); accept F32/F16 conv1d; and select the multimodal tensor
prefix from the archive so a flat text GGUF is not looked up under the
`model.language_model.` wrapper.

Verified: Qwen3.5-0.8B-Q4_K_M.gguf now boots on portable Metal and completes
"The capital of France is" -> "**Paris**. Located in the region of
Île-de-France" (coherent). Larger dense (27B) and MoE (35B-A3B) follow-ups:
V-head reorder for num_v_heads != num_k_heads, and fused-expert assembly.
Extends the qwen35/qwen35moe GGUF loader past the small dense smoke case:

- GQA V-head order: the converter permutes the linear-attention V heads
  grouped->tiled when num_v_heads != num_k_heads (so ggml's tiled
  broadcast aligns), while HF safetensors keep grouped order. Carry a
  `qwen35_linear_v_tiled` hparam (set for a GGUF source) and have the
  graph expand Q/K with a matching tiled `ggml_repeat` instead of the
  grouped `repeat_interleave`. The equal-head case is unaffected.

- MoE: read `expert_shared_feed_forward_length` into
  `shared_expert_intermediate_size`, and load the experts from the GGUF's
  three SEPARATE stacked tensors (`ffn_gate_exps` / `ffn_up_exps` /
  `ffn_down_exps`) plus the shared-expert tensors directly, rather than
  splitting a fused `mlp.experts.gate_up_proj` (which only the safetensors
  layout provides).

Verified on portable Metal: Qwen3.6-27B-Q4_K_M (dense, 64 layers) and
Qwen3.6-35B-A3B-UD-Q4_K_M (MoE, 256 experts + shared expert) both boot and
complete a prompt ("The capital of France is" -> "Paris"; coherent MoE
reasoning output).
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Qwen 3.5/3.6 GGUF support

Layer / File(s) Summary
GGUF archive contracts and tensor mappings
driver/portable/src/safetensors.hpp, driver/portable/src/gguf_archive.*
WeightArchive exposes is_gguf(), GGUFArchive returns true, and Qwen 3.5/3.6 tensor suffixes map to Hugging Face names.
Hybrid metadata and head expansion
driver/portable/src/hf_config.hpp, driver/portable/src/gguf_hparams.cpp, driver/portable/src/graph_qwen3_5.cpp
GGUF metadata populates hybrid attention parameters, layer types, shared-expert sizing, and tiled V-head behavior used during Q/K expansion.
GGUF model loading and inverse transforms
driver/portable/src/model.hpp, driver/portable/src/model.cpp
Model loading resolves wrapped prefixes, loads separated GGUF experts, supports unquantized small weights, applies A_log and folded-norm inversions, and uses transformed normalization tensors.

Sequence Diagram(s)

sequenceDiagram
  participant GGUFArchive
  participant Model
  participant Qwen35Loaders
  participant Qwen35Graph
  GGUFArchive->>Model: provide mapped tensors and GGUF metadata
  Model->>Qwen35Loaders: load experts and transformed small weights
  Qwen35Loaders->>Qwen35Graph: provide configured Qwen 3.5/3.6 tensors
  Qwen35Graph->>Qwen35Graph: expand Q/K according to V-head ordering
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: fixing Qwen 3.5 GGUF loading.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/qwen35-gguf-loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@driver/portable/src/model.cpp`:
- Around line 2250-2293: The GGUF type constants in read_small_weight_as_f32_
are incorrect, causing BF16 tensors to be rejected. Replace the local
TF32/TF16/TBF16 numeric enum and switch cases with the existing GGML_TYPE_F32,
GGML_TYPE_F16, and GGML_TYPE_BF16 constants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ed4fee8b-322a-46b1-b196-278518d4abb0

📥 Commits

Reviewing files that changed from the base of the PR and between b537a64 and b573120.

📒 Files selected for processing (8)
  • driver/portable/src/gguf_archive.cpp
  • driver/portable/src/gguf_archive.hpp
  • driver/portable/src/gguf_hparams.cpp
  • driver/portable/src/graph_qwen3_5.cpp
  • driver/portable/src/hf_config.hpp
  • driver/portable/src/model.cpp
  • driver/portable/src/model.hpp
  • driver/portable/src/safetensors.hpp

Comment on lines +2250 to +2293
// Read a small source weight (BF16 safetensors or BF16/F16/F32 GGUF) as a
// flat F32 vector. Used by the Qwen 3.5 synth helpers below, which need the
// raw float values to invert convert-time transforms. The element count is
// derived from the source dtype so it works regardless of how the tensor was
// stored. Throws on an unexpected (quantized) source.
static std::vector<float> read_small_weight_as_f32_(const StTensor& src,
const std::string& hf_name) {
// GGUF tensors carry their ggml type in `ggml_type_override`; safetensors
// leaves it -1 and uses `dtype`.
const bool is_gguf = src.ggml_type_override >= 0;
enum { TF32 = 0, TF16 = 1, TBF16 = 24 }; // ggml_type values
std::size_t elem_bytes;
int kind; // 0=f32, 1=f16, 2=bf16
if (is_gguf) {
switch (src.ggml_type_override) {
case TF32: elem_bytes = 4; kind = 0; break;
case TF16: elem_bytes = 2; kind = 1; break;
case TBF16: elem_bytes = 2; kind = 2; break;
default:
throw std::runtime_error(
"model qwen3_5: expected an unquantized small weight for "
+ hf_name);
}
} else if (src.dtype == StDtype::BF16) {
elem_bytes = 2; kind = 2;
} else if (src.dtype == StDtype::F32) {
elem_bytes = 4; kind = 0;
} else {
throw std::runtime_error(
"model qwen3_5: unsupported small-weight dtype for " + hf_name);
}
const std::size_t n = src.nbytes / elem_bytes;
std::vector<float> out(n);
if (kind == 0) {
std::memcpy(out.data(), src.data, n * sizeof(float));
} else if (kind == 2) {
const auto* p = reinterpret_cast<const std::uint16_t*>(src.data);
for (std::size_t i = 0; i < n; ++i) out[i] = bf16_to_f32(p[i]);
} else { // f16
const auto* p = reinterpret_cast<const std::uint16_t*>(src.data);
for (std::size_t i = 0; i < n; ++i) out[i] = ggml_fp16_to_fp32(p[i]);
}
return out;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Searching for ggml_type enum and override usage...\n'
rg -n "GGML_TYPE_BF16|ggml_type_override|enum ggml_type|typedef enum ggml_type|case GGML_TYPE_BF16|TBF16|GGML_TYPE_I8" driver/portable/src . -g '!**/node_modules/**' -g '!**/build/**' -g '!**/dist/**' || true

Repository: pie-project/pie

Length of output: 9350


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
# Intentionally empty placeholder to keep the request minimal.
PY

Repository: pie-project/pie

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Locating ggml_type enum definition...\n'
fd -a 'ggml.h' . || true
printf '\nSearching for enum members in likely headers...\n'
rg -n "GGML_TYPE_F32|GGML_TYPE_F16|GGML_TYPE_BF16|GGML_TYPE_I8|typedef enum ggml_type|enum ggml_type" . -g '*.h' -g '*.hpp' -g '*.c' -g '*.cpp' | head -n 200
printf '\nInspecting gguf_archive.cpp around type assignment...\n'
sed -n '260,350p' driver/portable/src/gguf_archive.cpp

Repository: pie-project/pie

Length of output: 18323


🌐 Web query:

GGML_TYPE_BF16 enum value ggml_type BF16 24 30 ggml.h

💡 Result:

In the GGML library, the enum constant GGML_TYPE_BF16 is defined with the value 30 [1][2][3]. This definition is part of the ggml_type enumeration found in ggml.h, which maps various data types (including floating-point, integer, and quantized formats) to specific integer values [1][2]. For reference, other types defined in the same enumeration include GGML_TYPE_I8 at 24 and various other types, which are organized to maintain backward compatibility [2][3]. The library uses this enumeration to manage tensor data types during operations [1][4].

Citations:


Use GGML_TYPE_BF16 here, not 24
ggml_type_override already stores the real GGUF type. 24 is GGML_TYPE_I8, so a BF16 small-weight tensor will fall into the default: path and fail to load. Switch on GGML_TYPE_F32 / GGML_TYPE_F16 / GGML_TYPE_BF16 directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@driver/portable/src/model.cpp` around lines 2250 - 2293, The GGUF type
constants in read_small_weight_as_f32_ are incorrect, causing BF16 tensors to be
rejected. Replace the local TF32/TF16/TBF16 numeric enum and switch cases with
the existing GGML_TYPE_F32, GGML_TYPE_F16, and GGML_TYPE_BF16 constants.

@ingim ingim merged commit 57ac28a into main Jul 11, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants