internal/autogen/image_moss_test.go
Rama main · réplica de lectura del repositorio original.
package autogen
import (
"path/filepath"
"strings"
"testing"
)
// FLUX.3 klein states txt_in 20480 = 8 x 2560. Under the old "any k <= 4" cap
// it matched nothing; a plain raised cap would hand it a 4 x 5120 or a 5 x 4096
// encoder first, because the caller prefers the lowest factor.
func TestAutogen_CaptionLayouts(t *testing.T) {
p := &EncoderPool{Files: []ComponentFile{
{Path: "/m/qwen3-32b.gguf", Role: RoleLlm, Family: "qwen3", Width: 5120, SizeGB: 30},
{Path: "/m/qwen3-8b.gguf", Role: RoleLlm, Family: "qwen3", Width: 4096, SizeGB: 8},
{Path: "/m/qwen3-vl-4b.gguf", Role: RoleLlm, Family: "qwen3vl", Width: 2560, SizeGB: 4},
}}
if got, _ := p.Llm(20480, false, ""); got != "/m/qwen3-vl-4b.gguf" {
t.Errorf("llm(20480) = %q, want the 2560-wide encoder (8 layers)", got)
}
if got, _ := p.Llm(12288, false, ""); got != "/m/qwen3-8b.gguf" {
t.Errorf("llm(12288) = %q, want the 4096-wide encoder (3 layers)", got)
}
for _, c := range []struct{ hidden, width, want int64 }{
{4096, 4096, 1}, {12288, 4096, 3}, {20480, 2560, 8},
{20480, 5120, 0}, {20480, 4096, 0}, {8192, 4096, 0},
} {
if got := captionFactor(c.hidden, c.width); got != c.want {
t.Errorf("captionFactor(%d, %d) = %d, want %d", c.hidden, c.width, got, c.want)
}
}
}
func TestAutogen_EncoderPoolRecurrentLlm(t *testing.T) {
p := &EncoderPool{Files: []ComponentFile{
{Path: "/m/MiMo-V2.6-Distill-Qwen-9B-BF16.gguf", Role: RoleRecurrentLlm, Family: "qwen35", Width: 4096, SizeGB: 18},
{Path: "/m/Qwen3.5-9B-Q8_0.gguf", Role: RoleRecurrentLlm, Family: "qwen35", Width: 4096, SizeGB: 9.5},
{Path: "/m/Qwen3.5-4B-Q6_K.gguf", Role: RoleRecurrentLlm, Family: "qwen35", Width: 2560, SizeGB: 3},
// A plain transformer of the same width is never a recurrent pick.
{Path: "/m/qwen3-8b.gguf", Role: RoleLlm, Family: "qwen3", Width: 4096, SizeGB: 8},
}}
if got := p.RecurrentLlm(4096, "qwen3.5-9b"); got != "/m/Qwen3.5-9B-Q8_0.gguf" {
t.Errorf("hinted = %q, want the hint to beat the bigger same-width distill", got)
}
if got := p.RecurrentLlm(2560); got != "/m/Qwen3.5-4B-Q6_K.gguf" {
t.Errorf("unhinted 2560 = %q", got)
}
if got := p.RecurrentLlm(12288); got != "" {
t.Errorf("no concatenation for recurrent encoders, got %q", got)
}
// The recurrent role stays invisible to every plain-LLM caller.
if got, _ := p.Llm(4096, false, ""); got != "/m/qwen3-8b.gguf" {
t.Errorf("Llm(4096) = %q, want the plain transformer only", got)
}
}
func TestAutogen_EncoderPoolTokenizer(t *testing.T) {
p := &EncoderPool{Files: []ComponentFile{
{Path: "/m/von-1.0/tokenizer.json", Role: RoleTokenizer},
{Path: "/m/unsloth/MOSS-Image-SFT-FP8/tokenizer/tokenizer.json", Role: RoleTokenizer},
}}
if got := p.Tokenizer("moss-image"); got != "/m/unsloth/MOSS-Image-SFT-FP8/tokenizer/tokenizer.json" {
t.Errorf("Tokenizer(moss-image) = %q", got)
}
// No fallback: an unrelated vocab is worse than a missing-component warning.
if got := p.Tokenizer("qwen3.5"); got != "" {
t.Errorf("Tokenizer(qwen3.5) = %q, want none", got)
}
}
// The scan collects tokenizer.json by name and keeps the FLUX.2 small decoder
// (same 32-ch latent, 384-wide decoder input) out of the VAE family.
func TestAutogen_ScanEncoderPoolTokenizerAndSmallDecoder(t *testing.T) {
dir := t.TempDir()
writeSafetensors(t, filepath.Join(dir, "flux2", "ae.safetensors"),
map[string][]int64{"decoder.conv_in.weight": {512, 32, 3, 3}})
writeSafetensors(t, filepath.Join(dir, "MOSS-Image-SFT-FP8", "vae", "diffusion_pytorch_model.safetensors"),
map[string][]int64{"decoder.conv_in.weight": {384, 32, 3, 3}})
writeSafetensors(t, filepath.Join(dir, "MOSS-Image-SFT-FP8", "tokenizer", "tokenizer.json"), nil)
p := ScanEncoderPool([]string{dir})
var vaes []string
tok := ""
for _, f := range p.Files {
switch f.Role {
case RoleVae:
vaes = append(vaes, filepath.ToSlash(f.Path))
case RoleTokenizer:
tok = filepath.ToSlash(f.Path)
}
}
if len(vaes) != 1 || !strings.HasSuffix(vaes[0], "flux2/ae.safetensors") {
t.Errorf("vaes = %v, want only the full FLUX.2 ae", vaes)
}
if !strings.HasSuffix(tok, "MOSS-Image-SFT-FP8/tokenizer/tokenizer.json") {
t.Errorf("tokenizer = %q", tok)
}
}
func TestAutogen_resolveComponentsMossAndFlux3(t *testing.T) {
pool := &EncoderPool{Files: []ComponentFile{
{Path: "/m/flux2/ae.safetensors", Role: RoleVae, Family: VaeFamilyFlux2},
{Path: "/m/Qwen3.5-9B-Q8_0.gguf", Role: RoleRecurrentLlm, Family: "qwen35", Width: 4096, SizeGB: 9.5},
{Path: "/m/MOSS-Image-SFT-FP8/tokenizer/tokenizer.json", Role: RoleTokenizer},
{Path: "/m/Qwen3VL-4B-Instruct-Q8_0.gguf", Role: RoleLlm, Family: "qwen3vl", Width: 2560, SizeGB: 4.3},
}}
c, missing := resolveComponents(EncoderSet{}, nil, "moss_image", "MOSS-Image-SFT-Q8_0", pool, 4096, videoInfo{})
if c.vae != "/m/flux2/ae.safetensors" || c.llm != "/m/Qwen3.5-9B-Q8_0.gguf" ||
c.tokenizer != "/m/MOSS-Image-SFT-FP8/tokenizer/tokenizer.json" || len(missing) != 0 {
t.Errorf("moss: %+v missing=%v", c, missing)
}
// Nothing recurrent or tokenizer on disk: both roles surface as missing,
// which is what the model browser's companion download keys on.
bare := &EncoderPool{Files: pool.Files[:1]}
if _, missing := resolveComponents(EncoderSet{}, nil, "moss_image", "MOSS-Image-SFT-Q8_0", bare, 4096, videoInfo{}); strings.Join(missing, ",") != "llm,tokenizer" {
t.Errorf("moss bare missing = %v, want [llm tokenizer]", missing)
}
c, missing = resolveComponents(EncoderSet{}, nil, "flux", "flux-3-image-klein-Q8_0", pool, 20480, videoInfo{})
if c.vae != "/m/flux2/ae.safetensors" || c.llm != "/m/Qwen3VL-4B-Instruct-Q8_0.gguf" || c.t5 != "" || c.clipL != "" || len(missing) != 0 {
t.Errorf("flux3: %+v missing=%v", c, missing)
}
if d := imageDefaultsFor("moss_image", "MOSS-Image-SFT-Q8_0", 4096); d.steps != 50 || d.cfg != 3 || d.flowShift != 5 {
t.Errorf("moss defaults = %+v", d)
}
if d := imageDefaultsFor("flux", "flux-3-image-klein-Q8_0", 20480); d.steps != 16 || d.cfg != 1 {
t.Errorf("flux3 defaults = %+v", d)
}
}
// Qwen3-4B and Qwen3-VL-4B are both 2560 wide, so width cannot say which a DiT
// wants. The real box that found this: a Qwen3-4B qwenLlm pin (for Z-Image)
// won FLUX.3's 8 x 2560 match, and with no pin the larger VL file won Z-Image's.
func TestAutogen_ImageEncoderArch(t *testing.T) {
const q3, vl = "/m/Qwen3-4B-Instruct-2507-Q4_K_M.gguf", "/m/Qwen3VL-4B-Instruct-Q8_0.gguf"
both := &EncoderPool{Files: []ComponentFile{
{Path: q3, Role: RoleLlm, Family: "qwen3", Width: 2560, SizeGB: 2.3},
{Path: vl, Role: RoleLlm, Family: "qwen3vl", Width: 2560, SizeGB: 4.0},
}}
cases := []struct {
label, arch, name string
cond int64
pin, want string
pool *EncoderPool
}{
{"flux3, q3 pin", "flux", "flux-3-image-klein-q8_0", 20480, q3, vl, both},
{"flux3, no pin", "flux", "flux-3-image-klein-q8_0", 20480, "", vl, both},
{"z-image, no pin", "lumina2", "z-image-turbo-q8_0", 2560, "", q3, both},
{"z-image, vl pin", "lumina2", "z-image-turbo-q8_0", 2560, vl, q3, both},
{"klein 4b, no pin", "flux", "flux-2-klein-4b-q8_0", 7680, "", q3, both},
// Only the impostor on disk: report the encoder missing rather than
// wire it (that is what makes the model browser offer the right one).
{"flux3, only q3", "flux", "flux-3-image-klein-q8_0", 20480, q3, "",
&EncoderPool{Files: both.Files[:1]}},
}
for _, c := range cases {
got, missing := resolveComponents(EncoderSet{QwenLlm: c.pin}, nil, c.arch, c.name, c.pool, c.cond, videoInfo{})
if got.llm != c.want {
t.Errorf("%s: llm = %q, want %q", c.label, got.llm, c.want)
}
if c.want == "" && !strings.Contains(strings.Join(missing, ","), "llm") {
t.Errorf("%s: missing = %v, want llm reported", c.label, missing)
}
}
}