Summary
A quantization job that finishes while no client is attached to its progress stream stays queued forever — in the API and in /app/quantize — while the finished artifact sits on disk.
Why
QuantizationService only advances job state from inside StreamProgress:
// core/services/quantization/service.go, inside StreamProgress
j.Status = update.Status
and state.json is written once when the job is created (saveJobState at StartJob) and never updated afterwards. With no SSE listener, nothing consumes the backend's progress stream, so no transition is ever recorded. The job state depends on an observer.
Reproduction
Two runs of the same model, same backend, same host.
Without a listener:
POST /api/quantization/jobs
{"model":"Qwen/Qwen3-0.6B","backend":"llama-cpp-quantization","quantization_type":"q4_k_m"}
Poll GET /api/quantization/jobs/<id> — "status":"queued" after 10 minutes and still today. Meanwhile:
13:16:50 job created, state.json written
13:16:51 hf_cache/ appears (download starts)
13:17:24 model-<type>.gguf written, 405 MB — the job is done
state.json on disk still reads "status": "queued", mtime 13:16:50.
With a listener (curl -sN .../progress running in parallel): the same job walks downloading → converting → quantizing → completed normally.
Cutting the stream mid-run freezes the reported status at that instant while the work continues — the F16 intermediate and the final GGUF were both written minutes after the last recorded status.
Impact
For a UI user the job looks stuck; for anything scripted against the API, a completed job is indistinguishable from a pending one. The artifact exists, the system does not know it.
Suggestion
Persist transitions where they happen rather than where they are observed — have the job runner update job.Status and rewrite state.json as the backend reports progress, and let StreamProgress be a pure reader.
Disclosure: this report was prepared with AI assistance (Claude); both runs, the file timestamps and the state.json inspection are from a real host (LocalAI v4.9.0-305-g8351db5de, ROCm 7.x / gfx1151).
Summary
A quantization job that finishes while no client is attached to its progress stream stays
queuedforever — in the API and in/app/quantize— while the finished artifact sits on disk.Why
QuantizationServiceonly advances job state from insideStreamProgress:and
state.jsonis written once when the job is created (saveJobStateatStartJob) and never updated afterwards. With no SSE listener, nothing consumes the backend's progress stream, so no transition is ever recorded. The job state depends on an observer.Reproduction
Two runs of the same model, same backend, same host.
Without a listener:
Poll
GET /api/quantization/jobs/<id>—"status":"queued"after 10 minutes and still today. Meanwhile:state.jsonon disk still reads"status": "queued", mtime 13:16:50.With a listener (
curl -sN .../progressrunning in parallel): the same job walksdownloading → converting → quantizing → completednormally.Cutting the stream mid-run freezes the reported status at that instant while the work continues — the F16 intermediate and the final GGUF were both written minutes after the last recorded status.
Impact
For a UI user the job looks stuck; for anything scripted against the API, a completed job is indistinguishable from a pending one. The artifact exists, the system does not know it.
Suggestion
Persist transitions where they happen rather than where they are observed — have the job runner update
job.Statusand rewritestate.jsonas the backend reports progress, and letStreamProgressbe a pure reader.Disclosure: this report was prepared with AI assistance (Claude); both runs, the file timestamps and the state.json inspection are from a real host (LocalAI v4.9.0-305-g8351db5de, ROCm 7.x / gfx1151).