Skip to content

Commit af58f80

Browse files
committed
ci(gallery): propose variant groupings for review on a schedule
A gallery entry may declare `variants:`, references to other entries that are alternative builds of the same weights, and auto-selection then installs the best build for the host. Those families exist only because humans curated them in two manual sweeps. The gallery agent dedupes on the HuggingFace repo URL and picks one quantization per model, so it never adds a second build of a repo it already has, and consequently never creates a family and never joins one. A model published across two repos lands as two unrelated standalone entries. The grouping decays as the gallery grows and nothing notices. Add a scheduled job, in the same shape as the checksum checker: compute offline, edit the index textually, open a pull request against ci-forks. It proposes and never decides. Grouping is a judgement call that has gone wrong in both directions, so the value is catching drift and surfacing candidates with their evidence. Three grouping signals, taken from the manual sweeps: same name once quantization markers are stripped, the `:` config-suffix convention, and the same primary weight filename once quantization markers are stripped. The third requires the same upstream repository. Excluding auxiliary files is not enough on its own: bert-embeddings, an ultravox audio model and a roleplay finetune all declare a primary file called llama-3.2-1b-instruct-q4_k_m.gguf, and grouping on that is the same error that linked four wan-2.1 entries and Z-Image-Turbo to qwen3-4b. Add gallery/variant-exclusions.yaml, a checked-in rejection ledger. A job that re-proposes declined candidates every night becomes noise and gets ignored. Declining a proposal is one flow-mapping line a reviewer adds inside the proposal pull request itself. Seeded with the six -abliterated pairs whose base is also in the gallery, the mistral-small multimodal pair, the whisper-1 alias, the kokoros language set, and the recurring finetune tokens. qat and apex are deliberately not on it: they are quantization techniques. Proposals refuse to nest, to let two parents claim one target, to target an entry that installs nothing, and to touch a merge anchor, since a variants key added to an anchor is inherited by every merging child. The anchor refusal names every entry that would inherit, which is the worklist a human needs. Run against the pre-sweep gallery, the job rediscovers 12 of the 19 groupings the second manual sweep made, with no false positives. The rest it reports as refusals or ledger declines rather than missing silently. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 2f33ad6 commit af58f80

13 files changed

Lines changed: 2374 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// RenderBody writes the pull request body.
9+
//
10+
// The body is the product of this job, not the diff. Grouping is a judgement
11+
// call that has gone wrong in both directions before, so a reviewer has to be
12+
// able to accept or reject each family from the body alone, without opening
13+
// HuggingFace to work out whether two entries hold the same weights.
14+
func RenderBody(r *Result, ledgerPath string) string {
15+
var b strings.Builder
16+
17+
b.WriteString("## Proposed gallery variant groupings\n\n")
18+
b.WriteString("This is a proposal, not a decision. The gallery agent adds one build per model and never joins an existing family, so entries that are alternative builds of the same weights drift apart as the gallery grows. This job re-applies the grouping heuristics from the manual sweeps and asks a human to confirm.\n\n")
19+
b.WriteString("Each family below lists the parent, the variants, and the evidence that they are the same weights. **Reject anything whose evidence you do not believe.**\n\n")
20+
b.WriteString(fmt.Sprintf("To decline a family permanently, add one line to `%s` in this pull request and close it:\n\n", ledgerPath))
21+
b.WriteString("```yaml\npairs:\n - {parent: some-model, variant: some-model-thing, reason: \"different finetune\"}\n```\n\n")
22+
23+
b.WriteString(fmt.Sprintf("### Proposed families (%d)\n\n", len(r.Families)))
24+
if len(r.Families) == 0 {
25+
b.WriteString("None.\n\n")
26+
}
27+
for _, f := range r.Families {
28+
b.WriteString(fmt.Sprintf("#### `%s`\n\n", f.Parent))
29+
b.WriteString("| variant | signals | evidence |\n|---|---|---|\n")
30+
for _, p := range f.Proposals {
31+
b.WriteString(fmt.Sprintf("| `%s` | %s | %s |\n", p.Variant, joinSignals(p.Evidence.Signals), describeEvidence(p.Evidence)))
32+
}
33+
b.WriteString("\n")
34+
}
35+
36+
b.WriteString(fmt.Sprintf("### Declined by the ledger (%d)\n\n", len(r.Suppressed)))
37+
if len(r.Suppressed) == 0 {
38+
b.WriteString("Nothing the heuristics found was already on the ledger.\n\n")
39+
} else {
40+
b.WriteString("Candidates the heuristics found and the ledger has already settled. They are listed so the ledger's effect stays visible rather than silently shrinking the job's output.\n\n")
41+
for _, s := range r.Suppressed {
42+
b.WriteString(fmt.Sprintf("- `%s` + `%s`: %s\n", s.A, s.B, s.Reason))
43+
}
44+
b.WriteString("\n")
45+
}
46+
47+
if len(r.AliasSkipped) > 0 {
48+
b.WriteString(fmt.Sprintf("### Aliases, not variants (%d)\n\n", len(r.AliasSkipped)))
49+
b.WriteString("These entries install byte for byte the same payload. An alias exists so clients can send a particular name; folding it under another entry would hide that name.\n\n")
50+
for _, s := range r.AliasSkipped {
51+
b.WriteString(fmt.Sprintf("- `%s` + `%s`: %s\n", s.A, s.B, s.Reason))
52+
}
53+
b.WriteString("\n")
54+
}
55+
56+
if len(r.Refusals) > 0 {
57+
b.WriteString(fmt.Sprintf("### Found but refused (%d)\n\n", len(r.Refusals)))
58+
b.WriteString("Candidates the heuristics found but the authoring rules would not let this job write. They need a human edit or a rule change.\n\n")
59+
for _, ref := range r.Refusals {
60+
b.WriteString(fmt.Sprintf("- %s: %s\n", codeList(ref.Members), ref.Reason))
61+
}
62+
b.WriteString("\n")
63+
}
64+
65+
b.WriteString("---\n\nOpened by `.github/ci/variantproposals`. Heuristics and the rejection ledger live there and in the ledger file; a wrong proposal is a bug in one of the two.\n")
66+
return b.String()
67+
}
68+
69+
func joinSignals(signals []Signal) string {
70+
if len(signals) == 0 {
71+
return "inferred through another member of the family"
72+
}
73+
out := make([]string, 0, len(signals))
74+
for _, s := range signals {
75+
out = append(out, "`"+string(s)+"`")
76+
}
77+
return strings.Join(out, ", ")
78+
}
79+
80+
func describeEvidence(e Evidence) string {
81+
var parts []string
82+
if e.SharedStem != "" {
83+
parts = append(parts, fmt.Sprintf("same name once quantization markers are stripped: `%s`", e.SharedStem))
84+
}
85+
if e.SharedFile != "" {
86+
parts = append(parts, fmt.Sprintf("same primary weight filename once quantization markers are stripped: `%s`", e.SharedFile))
87+
}
88+
if e.SharedRepo != "" {
89+
parts = append(parts, fmt.Sprintf("same upstream repo `%s`", e.SharedRepo))
90+
}
91+
if len(e.QuantTokens) > 0 {
92+
parts = append(parts, "differing quantization tokens: `"+strings.Join(e.QuantTokens, "`, `")+"`")
93+
}
94+
if len(parts) == 0 {
95+
return "reached this family through another member"
96+
}
97+
return strings.Join(parts, "; ")
98+
}
99+
100+
func codeList(names []string) string {
101+
out := make([]string, 0, len(names))
102+
for _, n := range names {
103+
out = append(out, "`"+n+"`")
104+
}
105+
return strings.Join(out, " + ")
106+
}
107+
108+
// RenderSummary is the terminal-facing digest of a run, so the workflow log
109+
// says what happened without anyone opening the pull request.
110+
func RenderSummary(r *Result) string {
111+
var b strings.Builder
112+
fmt.Fprintf(&b, "families proposed: %d\n", len(r.Families))
113+
for _, f := range r.Families {
114+
names := make([]string, 0, len(f.Proposals))
115+
for _, p := range f.Proposals {
116+
names = append(names, p.Variant)
117+
}
118+
fmt.Fprintf(&b, " %s <- %s\n", f.Parent, strings.Join(names, ", "))
119+
}
120+
fmt.Fprintf(&b, "declined by ledger: %d\n", len(r.Suppressed))
121+
for _, s := range r.Suppressed {
122+
fmt.Fprintf(&b, " %s\n", s)
123+
}
124+
fmt.Fprintf(&b, "aliases skipped: %d\n", len(r.AliasSkipped))
125+
for _, s := range r.AliasSkipped {
126+
fmt.Fprintf(&b, " %s\n", s)
127+
}
128+
fmt.Fprintf(&b, "refused: %d\n", len(r.Refusals))
129+
for _, ref := range r.Refusals {
130+
fmt.Fprintf(&b, " %s: %s\n", strings.Join(ref.Members, " + "), ref.Reason)
131+
}
132+
return b.String()
133+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"sort"
7+
"strings"
8+
)
9+
10+
var (
11+
inlineName = regexp.MustCompile(`^- (?:&\S+ )?name:`)
12+
keyName = regexp.MustCompile(`^ name:`)
13+
keyVariants = regexp.MustCompile(`^ variants:\s*(.*)$`)
14+
variantItem = regexp.MustCompile(`^ - `)
15+
unsafeInName = regexp.MustCompile(`[:#{}\[\],&*?|>'"%@` + "`" + `]|^\s|\s$`)
16+
)
17+
18+
// ApplyFamilies writes the proposed variant lists into the index text.
19+
//
20+
// The edit is textual on purpose. Re-serialising the index through a YAML
21+
// marshaller would reflow 40,000 lines, drop the anchors and merge keys the
22+
// gallery relies on, and produce a diff no reviewer could read, which would
23+
// make the pull request worthless even when the proposals inside it are right.
24+
func ApplyFamilies(ix *Index, families []Family) ([]string, error) {
25+
byName, _ := ix.ByName()
26+
27+
type edit struct {
28+
at int
29+
remove int
30+
insert []string
31+
ordinal int
32+
}
33+
var edits []edit
34+
35+
for _, f := range families {
36+
entry, ok := byName[strings.ToLower(f.Parent)]
37+
if !ok {
38+
return nil, fmt.Errorf("parent %q is not in the index", f.Parent)
39+
}
40+
items := make([]string, 0, len(f.Proposals))
41+
for _, p := range f.Proposals {
42+
items = append(items, " - model: "+quoteName(p.Variant))
43+
}
44+
45+
at, remove, err := insertionPoint(ix, entry)
46+
if err != nil {
47+
return nil, err
48+
}
49+
insert := items
50+
if remove > 0 || !hasVariantsKey(ix, entry) {
51+
insert = append([]string{" variants:"}, items...)
52+
}
53+
edits = append(edits, edit{at: at, remove: remove, insert: insert, ordinal: entry.Index})
54+
}
55+
56+
// Applying from the bottom up keeps every line number computed against the
57+
// original text valid while earlier edits are still pending.
58+
sort.Slice(edits, func(i, j int) bool { return edits[i].at > edits[j].at })
59+
60+
lines := append([]string(nil), ix.Lines...)
61+
for _, e := range edits {
62+
tail := append([]string(nil), lines[e.at+e.remove:]...)
63+
lines = append(lines[:e.at], append(append([]string(nil), e.insert...), tail...)...)
64+
}
65+
return lines, nil
66+
}
67+
68+
func hasVariantsKey(ix *Index, e *GalleryEntry) bool {
69+
for i := e.StartLine; i < e.EndLine; i++ {
70+
if keyVariants.MatchString(ix.Lines[i]) {
71+
return true
72+
}
73+
}
74+
return false
75+
}
76+
77+
// insertionPoint reports where new variant items belong, and how many existing
78+
// lines the insertion replaces.
79+
//
80+
// An entry with no variants key gets one right after its name, which is where
81+
// the hand-written families put it. An entry with an empty "variants: []" has
82+
// that line replaced by a block. An entry with a block gets its items appended.
83+
func insertionPoint(ix *Index, e *GalleryEntry) (at int, remove int, err error) {
84+
for i := e.StartLine; i < e.EndLine; i++ {
85+
m := keyVariants.FindStringSubmatch(ix.Lines[i])
86+
if m == nil {
87+
continue
88+
}
89+
if strings.TrimSpace(m[1]) == "[]" {
90+
return i, 1, nil
91+
}
92+
if strings.TrimSpace(m[1]) != "" {
93+
return 0, 0, fmt.Errorf("entry %q writes its variants inline (%q); this job only edits block lists", e.Name, strings.TrimSpace(m[1]))
94+
}
95+
last := i
96+
for j := i + 1; j < e.EndLine && variantItem.MatchString(ix.Lines[j]); j++ {
97+
last = j
98+
}
99+
return last + 1, 0, nil
100+
}
101+
102+
if inlineName.MatchString(ix.Lines[e.StartLine]) {
103+
return e.StartLine + 1, 0, nil
104+
}
105+
for i := e.StartLine; i < e.EndLine; i++ {
106+
if keyName.MatchString(ix.Lines[i]) {
107+
return i + 1, 0, nil
108+
}
109+
}
110+
return 0, 0, fmt.Errorf("entry %q has no name line to anchor the insertion to", e.Name)
111+
}
112+
113+
// quoteName quotes a variant reference when the name would otherwise change
114+
// meaning as bare YAML. Config-suffixed names carry a ":" and always need it.
115+
func quoteName(name string) string {
116+
if unsafeInName.MatchString(name) {
117+
return `"` + strings.ReplaceAll(name, `"`, `\"`) + `"`
118+
}
119+
return name
120+
}

0 commit comments

Comments
 (0)