mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-02 19:07:25 -05:00
ui : resume model downloads from the pause point
A stopped download keeps its partial file instead of deleting it, so re-posting the tag resumes from disk. The first progress record now carries the resume offset, the paused snapshot survives into the resumed entry, and a pause settles as soon as it is requested. Assisted-by: pi
This commit is contained in:
+7
-10
@@ -346,10 +346,6 @@ static int common_download_file_single_online(const std::string & url,
|
||||
const std::string path_temporary = path + ".downloadInProgress";
|
||||
int delay = retry_delay_seconds;
|
||||
|
||||
if (opts.callback) {
|
||||
opts.callback->on_start(p);
|
||||
}
|
||||
|
||||
for (int i = 0; i < max_attempts; ++i) {
|
||||
if (opts.callback && opts.callback->is_cancelled()) {
|
||||
break;
|
||||
@@ -373,6 +369,12 @@ static int common_download_file_single_online(const std::string & url,
|
||||
|
||||
p.downloaded = existing_size;
|
||||
|
||||
if (i == 0 && opts.callback) {
|
||||
// report after the resume offset is known, so a resumed file does not
|
||||
// start at 0 in the progress UI
|
||||
opts.callback->on_start(p);
|
||||
}
|
||||
|
||||
LOG_DBG("%s: downloading from %s to %s (etag:%s)...\n",
|
||||
__func__, common_http_show_masked_url(parts).c_str(),
|
||||
path_temporary.c_str(), etag.c_str());
|
||||
@@ -393,12 +395,7 @@ static int common_download_file_single_online(const std::string & url,
|
||||
if (opts.callback) {
|
||||
opts.callback->on_done(p, success);
|
||||
}
|
||||
if (opts.callback && opts.callback->is_cancelled() &&
|
||||
std::filesystem::exists(path_temporary)) {
|
||||
if (remove(path_temporary.c_str()) != 0) {
|
||||
LOG_ERR("%s: unable to delete temporary file: %s\n", __func__, path_temporary.c_str());
|
||||
}
|
||||
}
|
||||
// a stopped download keeps path_temporary, so the next attempt resumes from it
|
||||
if (!success) {
|
||||
LOG_ERR("%s: download failed after %d attempts\n", __func__, max_attempts);
|
||||
return -1; // max attempts reached
|
||||
|
||||
+2
-2
@@ -219,8 +219,8 @@
|
||||
{@render tooltipTrigger(tooltipText, deleteChip)}
|
||||
{:else if entry.isDownloading || entry.isPaused}
|
||||
<!-- in-flight / paused chips: the chip body pauses / resumes on click, the X
|
||||
inside the chip cancels (stops and discards the partial files). The X slot
|
||||
is reserved, so the chip never reflows when the affordance fades in -->
|
||||
inside the chip cancels the download. The X slot is reserved, so the chip
|
||||
never reflows when the affordance fades in -->
|
||||
<div
|
||||
class="group relative inline-flex h-auto items-center gap-1 overflow-hidden rounded-md! border px-2 py-1 text-left font-mono text-xs shadow-sm transition-[background-color,border-color,transform] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97]
|
||||
{entry.isPaused
|
||||
|
||||
@@ -59,6 +59,11 @@ function downloadIdKey(repoWithTag: string): string {
|
||||
return `${repo.toUpperCase()}:${tag.toUpperCase().replace(HF_UD_QUANT_PREFIX_REGEX, '')}`;
|
||||
}
|
||||
|
||||
/** Zeroed progress, used until the first feed record reports real counts. */
|
||||
function emptyDownloadProgress(): ModelDownloadProgress {
|
||||
return { downloadedBytes: 0, files: {}, totalBytes: 0 };
|
||||
}
|
||||
|
||||
export class ModelStatusManager {
|
||||
/**
|
||||
* Sidecar files pulled by registered models, as `<repo>/<file>` keys.
|
||||
@@ -250,8 +255,11 @@ export class ModelStatusManager {
|
||||
// the feed must be live so the resulting models_reload event refreshes the list
|
||||
this.subscribe();
|
||||
|
||||
// resuming a paused download: drop the paused state, and let the server
|
||||
// discard its stale DOWNLOADED entry (via the list fetch) before re-posting
|
||||
// resuming a paused download: keep the last reported progress so the chip does
|
||||
// not fall back to 0%, and let the server discard its stale DOWNLOADED entry
|
||||
// (via the list fetch) before re-posting
|
||||
const snapshot = this.pausedDownloads.get(repoWithTag) ?? null;
|
||||
|
||||
if (this.deletePausedDownload(repoWithTag) || this.stopRequests.delete(repoWithTag)) {
|
||||
await this.host.fetchRouterModels();
|
||||
}
|
||||
@@ -264,7 +272,7 @@ export class ModelStatusManager {
|
||||
}
|
||||
|
||||
// flip the chip to "downloading" right away; the feed refines it with real progress
|
||||
this.downloadProgress.set(repoWithTag, { downloadedBytes: 0, files: {}, totalBytes: 0 });
|
||||
this.downloadProgress.set(repoWithTag, snapshot ?? emptyDownloadProgress());
|
||||
|
||||
toast.success(`Download started: ${this.host.toDisplayName(repoWithTag)}`);
|
||||
} catch (error) {
|
||||
@@ -378,9 +386,10 @@ export class ModelStatusManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* The server stops the download child but keeps the partial files, so
|
||||
* re-posting the tag resumes where it stopped. The feed reports the stop
|
||||
* as download_failed; the 'pause' stop request marks it as intentional.
|
||||
* The server stops the download child but keeps the partial file, so re-posting
|
||||
* the tag resumes where it stopped. The feed reports the stop as download_failed;
|
||||
* the 'pause' stop request marks it as intentional, and the paused state is stored
|
||||
* before the request: an unconfirmed pause would leave the chip spinning.
|
||||
*/
|
||||
async pauseDownload(repoWithTag: string): Promise<void> {
|
||||
if (!serverStore.isRouterMode) {
|
||||
@@ -393,10 +402,21 @@ export class ModelStatusManager {
|
||||
|
||||
this.stopRequests.set(repoWithTag, ModelDownloadStopRequest.PAUSE);
|
||||
|
||||
const snapshot = this.downloadProgress.get(repoWithTag) ?? null;
|
||||
const wasInFlight = this.downloadProgress.delete(repoWithTag);
|
||||
|
||||
this.setPausedDownload(repoWithTag, snapshot);
|
||||
|
||||
try {
|
||||
await ModelsService.unload(repoWithTag);
|
||||
} catch {
|
||||
this.stopRequests.delete(repoWithTag);
|
||||
this.deletePausedDownload(repoWithTag);
|
||||
|
||||
if (wasInFlight) {
|
||||
this.downloadProgress.set(repoWithTag, snapshot ?? emptyDownloadProgress());
|
||||
}
|
||||
|
||||
toast.error(`Failed to pause: ${repoWithTag}`);
|
||||
}
|
||||
}
|
||||
@@ -476,7 +496,11 @@ export class ModelStatusManager {
|
||||
}
|
||||
|
||||
if (request === ModelDownloadStopRequest.PAUSE) {
|
||||
this.setPausedDownload(event.model, progress);
|
||||
// a pause already stored its snapshot when the user asked for it
|
||||
if (!this.pausedDownloads.has(event.model)) {
|
||||
this.setPausedDownload(event.model, progress);
|
||||
}
|
||||
|
||||
this.failedDownloads.delete(event.model);
|
||||
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user