mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-31 02:18:50 -04:00
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> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
134 lines
5.3 KiB
Go
134 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// RenderBody writes the pull request body.
|
|
//
|
|
// The body is the product of this job, not the diff. Grouping is a judgement
|
|
// call that has gone wrong in both directions before, so a reviewer has to be
|
|
// able to accept or reject each family from the body alone, without opening
|
|
// HuggingFace to work out whether two entries hold the same weights.
|
|
func RenderBody(r *Result, ledgerPath string) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString("## Proposed gallery variant groupings\n\n")
|
|
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")
|
|
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")
|
|
b.WriteString(fmt.Sprintf("To decline a family permanently, add one line to `%s` in this pull request and close it:\n\n", ledgerPath))
|
|
b.WriteString("```yaml\npairs:\n - {parent: some-model, variant: some-model-thing, reason: \"different finetune\"}\n```\n\n")
|
|
|
|
b.WriteString(fmt.Sprintf("### Proposed families (%d)\n\n", len(r.Families)))
|
|
if len(r.Families) == 0 {
|
|
b.WriteString("None.\n\n")
|
|
}
|
|
for _, f := range r.Families {
|
|
b.WriteString(fmt.Sprintf("#### `%s`\n\n", f.Parent))
|
|
b.WriteString("| variant | signals | evidence |\n|---|---|---|\n")
|
|
for _, p := range f.Proposals {
|
|
b.WriteString(fmt.Sprintf("| `%s` | %s | %s |\n", p.Variant, joinSignals(p.Evidence.Signals), describeEvidence(p.Evidence)))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
b.WriteString(fmt.Sprintf("### Declined by the ledger (%d)\n\n", len(r.Suppressed)))
|
|
if len(r.Suppressed) == 0 {
|
|
b.WriteString("Nothing the heuristics found was already on the ledger.\n\n")
|
|
} else {
|
|
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")
|
|
for _, s := range r.Suppressed {
|
|
b.WriteString(fmt.Sprintf("- `%s` + `%s`: %s\n", s.A, s.B, s.Reason))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
if len(r.AliasSkipped) > 0 {
|
|
b.WriteString(fmt.Sprintf("### Aliases, not variants (%d)\n\n", len(r.AliasSkipped)))
|
|
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")
|
|
for _, s := range r.AliasSkipped {
|
|
b.WriteString(fmt.Sprintf("- `%s` + `%s`: %s\n", s.A, s.B, s.Reason))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
if len(r.Refusals) > 0 {
|
|
b.WriteString(fmt.Sprintf("### Found but refused (%d)\n\n", len(r.Refusals)))
|
|
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")
|
|
for _, ref := range r.Refusals {
|
|
b.WriteString(fmt.Sprintf("- %s: %s\n", codeList(ref.Members), ref.Reason))
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
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")
|
|
return b.String()
|
|
}
|
|
|
|
func joinSignals(signals []Signal) string {
|
|
if len(signals) == 0 {
|
|
return "inferred through another member of the family"
|
|
}
|
|
out := make([]string, 0, len(signals))
|
|
for _, s := range signals {
|
|
out = append(out, "`"+string(s)+"`")
|
|
}
|
|
return strings.Join(out, ", ")
|
|
}
|
|
|
|
func describeEvidence(e Evidence) string {
|
|
var parts []string
|
|
if e.SharedStem != "" {
|
|
parts = append(parts, fmt.Sprintf("same name once quantization markers are stripped: `%s`", e.SharedStem))
|
|
}
|
|
if e.SharedFile != "" {
|
|
parts = append(parts, fmt.Sprintf("same primary weight filename once quantization markers are stripped: `%s`", e.SharedFile))
|
|
}
|
|
if e.SharedRepo != "" {
|
|
parts = append(parts, fmt.Sprintf("same upstream repo `%s`", e.SharedRepo))
|
|
}
|
|
if len(e.QuantTokens) > 0 {
|
|
parts = append(parts, "differing quantization tokens: `"+strings.Join(e.QuantTokens, "`, `")+"`")
|
|
}
|
|
if len(parts) == 0 {
|
|
return "reached this family through another member"
|
|
}
|
|
return strings.Join(parts, "; ")
|
|
}
|
|
|
|
func codeList(names []string) string {
|
|
out := make([]string, 0, len(names))
|
|
for _, n := range names {
|
|
out = append(out, "`"+n+"`")
|
|
}
|
|
return strings.Join(out, " + ")
|
|
}
|
|
|
|
// RenderSummary is the terminal-facing digest of a run, so the workflow log
|
|
// says what happened without anyone opening the pull request.
|
|
func RenderSummary(r *Result) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "families proposed: %d\n", len(r.Families))
|
|
for _, f := range r.Families {
|
|
names := make([]string, 0, len(f.Proposals))
|
|
for _, p := range f.Proposals {
|
|
names = append(names, p.Variant)
|
|
}
|
|
fmt.Fprintf(&b, " %s <- %s\n", f.Parent, strings.Join(names, ", "))
|
|
}
|
|
fmt.Fprintf(&b, "declined by ledger: %d\n", len(r.Suppressed))
|
|
for _, s := range r.Suppressed {
|
|
fmt.Fprintf(&b, " %s\n", s)
|
|
}
|
|
fmt.Fprintf(&b, "aliases skipped: %d\n", len(r.AliasSkipped))
|
|
for _, s := range r.AliasSkipped {
|
|
fmt.Fprintf(&b, " %s\n", s)
|
|
}
|
|
fmt.Fprintf(&b, "refused: %d\n", len(r.Refusals))
|
|
for _, ref := range r.Refusals {
|
|
fmt.Fprintf(&b, " %s: %s\n", strings.Join(ref.Members, " + "), ref.Reason)
|
|
}
|
|
return b.String()
|
|
}
|