The catalog had one source a model, so an embedding model served by vLLM from its repository on a card and by llama.cpp from a GGUF on CPUs had its entry swapped by hand halfway through the metallurgy import. A profile's source is what its engine loads and placement looks for; the model's is the default, and what updates and canaries move. ctl model add checks every source, the GGUF check reads each llama.cpp profile's, and ctl weights collects none a profile loads. An empty source leaves a profile's fingerprint as it was, so running replicas do not read as stale. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
91 lines
3.2 KiB
Go
91 lines
3.2 KiB
Go
package catalog
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// embedModel is an embedding model the way it was wanted on the PoC: vLLM
|
|
// on the card from the repository, llama.cpp on CPUs from a GGUF.
|
|
func embedModel() Model {
|
|
return Model{Name: "qwen3-embed-4b", Source: "Qwen/Qwen3-Embedding-4B",
|
|
Profiles: []Profile{
|
|
{Name: "gpu", Engine: "vllm", GPUClass: "6000-24g", TP: 1},
|
|
{Name: "cpu", Engine: "llamacpp", Source: "hf:Qwen/Qwen3-Embedding-4B-GGUF/Qwen3-Embedding-4B-Q8_0.gguf"},
|
|
}}
|
|
}
|
|
|
|
func TestEachProfileLoadsItsOwnSource(t *testing.T) {
|
|
// The catalog had one source a model, so serving an embedding model by
|
|
// vLLM on a card and by llama.cpp on CPUs meant swapping the entry by
|
|
// hand halfway through an import.
|
|
cat, err := New([]Model{embedModel()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, _ := cat.Resolve("qwen3-embed-4b")
|
|
gpu, _ := m.EngineProfile("gpu")
|
|
cpu, _ := m.EngineProfile("cpu")
|
|
if gpu.ModelPath != "Qwen/Qwen3-Embedding-4B" || !strings.HasSuffix(cpu.ModelPath, "Q8_0.gguf") {
|
|
t.Errorf("gpu loads %q, cpu %q", gpu.ModelPath, cpu.ModelPath)
|
|
}
|
|
for _, p := range m.PlacementProfiles() {
|
|
if want := map[string]string{"gpu": gpu.ModelPath, "cpu": cpu.ModelPath}[p.Name]; p.Source != want {
|
|
t.Errorf("placement for %s looks for %q, want %q", p.Name, p.Source, want)
|
|
}
|
|
}
|
|
if got := m.WeightSources(); len(got) != 2 || !slices.Contains(got, cpu.ModelPath) {
|
|
t.Errorf("weight sources %v: a profile's own must not be collected", got)
|
|
}
|
|
}
|
|
|
|
func TestAProfileWithoutASourceKeepsItsFingerprint(t *testing.T) {
|
|
// Replicas are compared with their profile by fingerprint; a field added
|
|
// to every profile must not make every running replica look stale.
|
|
p := Profile{Name: "gpu", Engine: "vllm", GPUClass: "6000-24g", TP: 1}
|
|
before := p.Fingerprint()
|
|
if strings.Contains(mustJSON(t, p), "source") {
|
|
t.Error("an empty source is written into the profile")
|
|
}
|
|
p.Source = "Qwen/Other"
|
|
if p.Fingerprint() == before {
|
|
t.Error("a profile whose source changed reads as unchanged")
|
|
}
|
|
}
|
|
|
|
func TestAProfileSourceIsCheckedAsTheModelsIs(t *testing.T) {
|
|
page := embedModel()
|
|
page.Profiles[1].Source = "https://huggingface.co/Qwen/Qwen3-Embedding-4B-GGUF"
|
|
if _, err := New([]Model{page}); err == nil || !strings.Contains(err.Error(), `profile "cpu"`) {
|
|
t.Errorf("a repository page for llama.cpp: %v", err)
|
|
}
|
|
// The model's own source may be a page vLLM reads as a repository, now
|
|
// that the llama.cpp profile loads something else.
|
|
repo := embedModel()
|
|
repo.Source = "https://huggingface.co/Qwen/Qwen3-Embedding-4B"
|
|
cat, err := New([]Model{repo})
|
|
if err != nil {
|
|
t.Fatalf("a repository page for the vLLM profile beside a llama.cpp one with its own GGUF: %v", err)
|
|
}
|
|
m, _ := cat.Resolve("qwen3-embed-4b")
|
|
if m.Source != "Qwen/Qwen3-Embedding-4B" {
|
|
t.Errorf("the page was not read as the repository: %q", m.Source)
|
|
}
|
|
trt := embedModel()
|
|
trt.Profiles[0] = Profile{Name: "gpu", Engine: "trtllm", GPUClass: "6000-24g", TP: 1, Source: "hf:a/b/m.gguf"}
|
|
if _, err := New([]Model{trt}); err == nil || !strings.Contains(err.Error(), "not a .gguf") {
|
|
t.Errorf("trtllm given a GGUF by its profile: %v", err)
|
|
}
|
|
}
|
|
|
|
func mustJSON(t *testing.T, v any) string {
|
|
t.Helper()
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(b)
|
|
}
|