From a89d780707a82eb7fbade924f9e02397b80c37a0 Mon Sep 17 00:00:00 2001 From: "mudler's LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Sat, 18 Jul 2026 00:01:24 +0200 Subject: [PATCH] fix(gallery): keep multi-file HF install progress proportional during verify (#10908) The artifact progress bridge mapped every PhaseVerifying event to a flat 95%. The materializer emits PhaseVerifying once per file (from each file's AfterDownload hook) and downloads run sequentially, so the first small file to finish pinned the bar at 95% - and, because progress is monotonic, it stayed at 95% for the entire remaining download (e.g. a 70GB checkpoint reporting 95% at 410MB / 69.7GB). Track per-file verify proportionally to the running aggregate bytes, the same way downloading does. CurrentBytes already reflects "completed files + this file", so the percentage advances honestly. The flat 95%/99% is now reserved for the genuinely once-per-install Committing/Persisting phases. Signed-off-by: Ettore Di Giacinto Co-authored-by: Ettore Di Giacinto --- core/services/galleryop/artifact_progress.go | 10 +++++++++- .../galleryop/artifact_progress_test.go | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/core/services/galleryop/artifact_progress.go b/core/services/galleryop/artifact_progress.go index d4e49be58..afda9766a 100644 --- a/core/services/galleryop/artifact_progress.go +++ b/core/services/galleryop/artifact_progress.go @@ -33,7 +33,15 @@ func (b *artifactProgressBridge) Sink(event modelartifacts.ProgressEvent) { } message = fmt.Sprintf("Downloading model file: %s", event.File) case modelartifacts.PhaseVerifying: - progress = max(progress, 95) + // Verification runs per file — the materializer emits this from each + // file's AfterDownload hook, not once at the end. CurrentBytes is the + // running aggregate (completed files + this file), so track it + // proportionally like downloading. A flat 95% here pinned the bar the + // moment the first file finished, leaving a multi-file (e.g. 70GB) + // install stuck at 95% for the entire remaining download. + if event.TotalBytes > 0 { + progress = max(progress, min(90, float64(event.CurrentBytes)*90/float64(event.TotalBytes))) + } message = "Verifying model files" case modelartifacts.PhaseCommitting: progress = max(progress, 99) diff --git a/core/services/galleryop/artifact_progress_test.go b/core/services/galleryop/artifact_progress_test.go index fa97eb3b9..b46ddaa49 100644 --- a/core/services/galleryop/artifact_progress_test.go +++ b/core/services/galleryop/artifact_progress_test.go @@ -30,4 +30,23 @@ var _ = Describe("artifact operation progress", func() { Expect(statuses[5].Progress).To(Equal(float64(99))) Expect(statuses[5].CurrentBytes).To(Equal(int64(100))) }) + + It("keeps a per-file verify proportional to bytes, not a flat 95%", func() { + // A multi-file model (e.g. a 70GB checkpoint) downloads sequentially and + // verifies each file from its AfterDownload hook. When the first small + // file finishes, a PhaseVerifying event fires while nearly all bytes are + // still to download — the reported percentage must reflect the tiny + // fraction completed, not slam the bar to 95% for the rest of the run. + var last *OpStatus + bridge := newArtifactProgressBridge(func(status *OpStatus) { + copy := *status + last = © + }) + bridge.Sink(modelartifacts.ProgressEvent{Phase: modelartifacts.PhaseResolving, TotalBytes: 70000}) + bridge.Sink(modelartifacts.ProgressEvent{Phase: modelartifacts.PhaseDownloading, File: "config.json", CurrentBytes: 400, TotalBytes: 70000}) + bridge.Sink(modelartifacts.ProgressEvent{Phase: modelartifacts.PhaseVerifying, File: "config.json", CurrentBytes: 400, TotalBytes: 70000}) + + Expect(last.Phase).To(Equal("verifying")) + Expect(last.Progress).To(BeNumerically("<", 5)) + }) })