Skip to content

fix(quantization): pin the producing backend on imported quantized models (#11875) - #11879

Open
Anai-Guo wants to merge 1 commit into
mudler:masterfrom
Anai-Guo:fix-quant-import-backend
Open

fix(quantization): pin the producing backend on imported quantized models (#11875)#11879
Anai-Guo wants to merge 1 commit into
mudler:masterfrom
Anai-Guo:fix-quant-import-backend

Conversation

@Anai-Guo

@Anai-Guo Anai-Guo commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #11875.

Problem

QuantizationService.ImportModel copies the finished GGUF into the models directory and then calls importers.ImportLocalPath to generate the config. That importer detects the file format and hardcodes the backend for any GGUF:

// core/gallery/importers/local.go
cfg := &config.ModelConfig{
    Name:                name,
    Backend:             "llama-cpp",     // <- always, for any .gguf
    ...
}
cfg.Description = buildDescription(dirPath, "GGUF")   // "Fine-tuned model (GGUF)"

ImportModel then only overrides cfg.Name, so the job's own backend is discarded. For a model this service just quantized with a backend whose weight types stock llama.cpp does not know (the reporter hit this with rocmfp4, ROCmFP4 types from #11636), the generated YAML names an engine that cannot load the file. The import reports success and registers a model that fails at load; correcting backend: by hand makes the same file work immediately.

The one path that produces a model and the one path that registers it disagree about how to run it.

Fix

The job record already carries the backend that served StartQuantization (schema.QuantizationJob.Backend), so carry it into the config instead of keeping the detected default.

Copying job.Backend verbatim would be wrong for the common case, because the gallery publishes a quantizer as a release channel of the engine that serves its output — llama-cpp-quantization is llama.cpp's quantizer, and the GGUF it writes is served by llama-cpp. That convention is already stated in core/config/backend_capabilities.go:

// galleryChannelSuffixes are the release-channel suffixes appended to a backend
// name in the gallery ("llama-cpp" vs "llama-cpp-development" vs
// "llama-cpp-quantization"). They carry no engine information, so they are
// stripped before any family or capability lookup falls back.
var galleryChannelSuffixes = []string{"-development", "-quantization"}

So the new helper strips that suffix (after config.NormalizeBackendName, which folds llama.cppllama-cpp):

job.Backend pinned backend:
llama-cpp-quantization llama-cpp channel suffix stripped — the reporter's first case
rocmfp4 rocmfp4 quantizes and serves; no suffix, unchanged — the reporter's failing case
rocm-rocmfp4-quantization rocm-rocmfp4 pinned hardware variant, a valid backend: value
llama.cpp-quantization llama-cpp dot-form normalized
"" (unchanged) detected default is kept

Both cases the reporter actually hit come out right.

I deliberately kept this inside core/services/quantization. stripBackendVariant in core/config does the same job and also strips hardware prefixes, but it is unexported; if you would rather export it and call it here (which would additionally reduce rocm-rocmfp4 to rocmfp4), say so and I will switch — I did not want to widen a package's API unasked.

Also addresses the secondary point in the issue: the description said Fine-tuned model (GGUF) for a model that was quantized, not fine-tuned. This path now writes Quantized model (<type>, GGUF), guarded so an empty type leaves the importer's text alone. The generic importer is untouched.

Verification

Added five specs to core/services/quantization/service_test.go (existing ginkgo white-box suite) covering exactly the table above.

The mapping was also run standalone against the real logic:

OK  "llama-cpp-quantization"       -> "llama-cpp"     want "llama-cpp"
OK  "rocmfp4"                      -> "rocmfp4"       want "rocmfp4"
OK  "rocm-rocmfp4-quantization"    -> "rocm-rocmfp4"  want "rocm-rocmfp4"
OK  "llama.cpp-quantization"       -> "llama-cpp"     want "llama-cpp"
OK  ""                             -> ""              want ""
all passed: true

gofmt -l is clean on both changed files. I do not have a ROCm host, so the end-to-end quantize→import round trip on rocmfp4 is not something I could re-run; the failing artifact and the hand-corrected YAML in the issue are the ground truth I worked from.


🤖 Generated with Claude Code

…dels (mudler#11875)

ImportModel hands the copied GGUF to importers.ImportLocalPath, which detects
the file format and defaults every GGUF to `backend: llama-cpp`. For a model
this service just produced with a backend stock llama.cpp cannot read, the
generated config names an engine that cannot load the file, and the import
silently registers an unloadable model. Correcting `backend:` by hand makes the
same file work.

The job record already carries the backend that served StartQuantization, so
carry it into the config instead of keeping the detected default. The gallery
publishes a quantizer as a release channel of the engine that runs its output
("llama-cpp-quantization" is llama.cpp's quantizer, whose GGUF is served by
"llama-cpp"), so the channel suffix is stripped to get the serving backend.
A backend that both quantizes and serves ("rocmfp4") carries no suffix and
passes through unchanged, as do pinned hardware variants ("rocm-rocmfp4"),
which are valid values for a config's backend field. An empty job backend
leaves the detected default in place.

Also replace the importer's generic "Fine-tuned model (GGUF)" description for
this path: the model was quantized, not fine-tuned, and the job knows the type.

Signed-off-by: Tai An <antai12232931@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Quantization import always writes backend: llama-cpp, ignoring the backend that produced the model

1 participant