convert: fixes for qwen3next model conversion (#16354)

This change addresses some problems with GGUF conversion including:
 * correctly naming the MoE tensors
 * correctly quantizing the nextn.eh_proj.weight MTP tensor
This commit is contained in:
Patrick Devine
2026-06-01 09:43:11 -07:00
committed by GitHub
parent e7766a4a47
commit 0e93ccc2cd
4 changed files with 205 additions and 14 deletions
+56 -14
View File
@@ -934,8 +934,11 @@ func safetensorFloat32Data(st safetensor) ([]float32, error) {
func (q *qwen3NextModel) Tensors(ts []Tensor) []*ggml.Tensor {
var out []*ggml.Tensor
merges := make([]merge, q.NumHiddenLayers*3)
for i := range q.NumHiddenLayers {
ts = q.renameMTPLayerTensors(ts)
blockCount := q.NumHiddenLayers + q.NumNextNPredictLayers
merges := make([]merge, blockCount*3)
for i := range blockCount {
merges[i*3+0] = merge{
fmt.Sprintf("blk.%d.mlp.experts.*.gate_proj.weight", i),
fmt.Sprintf("blk.%d.ffn_gate_exps.weight", i),
@@ -1065,6 +1068,57 @@ func (q *qwen3NextModel) Tensors(ts []Tensor) []*ggml.Tensor {
return out
}
func (q *qwen3NextModel) renameMTPLayerTensors(ts []Tensor) []Tensor {
var out []Tensor
for i, t := range ts {
name, ok := q.mtpLayerTensorName(t.Name())
if !ok {
continue
}
if out == nil {
out = slices.Clone(ts)
}
out[i] = &renamedTensor{Tensor: t, name: name}
}
if out != nil {
return out
}
return ts
}
func (q *qwen3NextModel) mtpLayerTensorName(name string) (string, bool) {
rest := strings.TrimPrefix(name, "mtp.layers.")
if rest == name {
return "", false
}
layer, suffix, ok := strings.Cut(rest, ".")
if !ok {
return "", false
}
idx, err := strconv.ParseUint(layer, 10, 32)
if err != nil {
return "", false
}
return fmt.Sprintf("blk.%d.%s", q.NumHiddenLayers+uint32(idx), suffix), true
}
type renamedTensor struct {
Tensor
name string
}
func (t *renamedTensor) Name() string {
return t.name
}
func (t *renamedTensor) Clone() Tensor {
return &renamedTensor{Tensor: t.Tensor.Clone(), name: t.name}
}
func (t *renamedTensor) SourceDType() string {
return sourceDType(t.Tensor)
}
func (q *qwen3NextModel) appendDirectTensor(out []*ggml.Tensor, t Tensor, name string) []*ggml.Tensor {
if qwen3NextShouldShiftNorm(name) {
t = t.Clone()
@@ -1093,18 +1147,6 @@ func (q *qwen3NextModel) mtpTensorNames(name string) []string {
nextn = 1
}
if rest := strings.TrimPrefix(name, "mtp.layers."); rest != name {
layer, suffix, ok := strings.Cut(rest, ".")
if !ok {
return nil
}
idx, err := strconv.ParseUint(layer, 10, 32)
if err != nil {
return nil
}
return []string{fmt.Sprintf("blk.%d.%s", base+uint32(idx), suffix)}
}
var suffix string
switch name {
case "mtp.fc.weight":
+95
View File
@@ -819,6 +819,101 @@ func TestQwen35MoePackedExperts(t *testing.T) {
}
}
func TestQwen35MTPMoePackedExperts(t *testing.T) {
m := &qwen3NextModel{
qwen3NextTextConfig: qwen3NextTextConfig{
NumHiddenLayers: 40,
NumNextNPredictLayers: 1,
},
}
out := m.Tensors([]Tensor{
&fakeTensor{
name: "mtp.layers.0.mlp.experts.gate_up_proj",
shape: []uint64{2, 4, 3},
data: []float32{
0, 1, 2,
3, 4, 5,
6, 7, 8,
9, 10, 11,
12, 13, 14,
15, 16, 17,
18, 19, 20,
21, 22, 23,
},
},
&fakeTensor{
name: "mtp.layers.0.mlp.experts.down_proj",
shape: []uint64{2, 5, 3},
data: make([]float32, 2*5*3),
},
})
byName := map[string]*ggml.Tensor{}
for _, tensor := range out {
if strings.Contains(tensor.Name, ".mlp.experts.") {
t.Fatalf("unexpected raw expert tensor %q", tensor.Name)
}
byName[tensor.Name] = tensor
}
gate := byName["blk.40.ffn_gate_exps.weight"]
if gate == nil {
t.Fatalf("missing tensor %q", "blk.40.ffn_gate_exps.weight")
}
if got, want := gate.Shape, []uint64{2, 2, 3}; !slices.Equal(got, want) {
t.Fatalf("unexpected gate shape: got %v want %v", got, want)
}
if got, want := readTensorData(t, gate), []float32{
0, 1, 2, 3, 4, 5,
12, 13, 14, 15, 16, 17,
}; !slices.Equal(got, want) {
t.Fatalf("unexpected gate values: got %v want %v", got, want)
}
if _, ok := byName["blk.40.ffn_up_exps.weight"]; !ok {
t.Fatalf("missing tensor %q", "blk.40.ffn_up_exps.weight")
}
if _, ok := byName["blk.40.ffn_down_exps.weight"]; !ok {
t.Fatalf("missing tensor %q", "blk.40.ffn_down_exps.weight")
}
}
func TestQwen35MTPMoePerExpertTensors(t *testing.T) {
m := &qwen3NextModel{
qwen3NextTextConfig: qwen3NextTextConfig{
NumHiddenLayers: 40,
NumNextNPredictLayers: 1,
},
}
out := m.Tensors([]Tensor{
&fakeTensor{
name: "mtp.layers.0.mlp.experts.1.gate_proj.weight",
shape: []uint64{2, 2},
data: []float32{10, 11, 12, 13},
},
&fakeTensor{
name: "mtp.layers.0.mlp.experts.0.gate_proj.weight",
shape: []uint64{2, 2},
data: []float32{0, 1, 2, 3},
},
})
if len(out) != 1 {
t.Fatalf("unexpected output tensor count: got %d want 1", len(out))
}
if got, want := out[0].Name, "blk.40.ffn_gate_exps.weight"; got != want {
t.Fatalf("unexpected tensor name: got %q want %q", got, want)
}
if got, want := out[0].Shape, []uint64{2, 2, 2}; !slices.Equal(got, want) {
t.Fatalf("unexpected tensor shape: got %v want %v", got, want)
}
if got, want := readTensorData(t, out[0]), []float32{0, 1, 2, 3, 10, 11, 12, 13}; !slices.Equal(got, want) {
t.Fatalf("unexpected tensor values: got %v want %v", got, want)
}
}
func TestQwen35SharedExpertGateKeepsMatrixShape(t *testing.T) {
m := &qwen3NextModel{}