Skip to content

Commit 2e485e5

Browse files
committed
Add resumable downloads for llama-server model loading
- Implement resumable downloads in common_download_file_single function - Add detection of partial download files (.downloadInProgress) - Check server support for HTTP Range requests via Accept-Ranges header - Implement HTTP Range request with "bytes=<start>-" header - Open files in append mode when resuming vs create mode for new downloads Signed-off-by: Eric Curtin <[email protected]>
1 parent 55758b0 commit 2e485e5

File tree

1 file changed

+65
-20
lines changed

1 file changed

+65
-20
lines changed

common/arg.cpp

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,18 @@ static std::string read_file(const std::string & fname) {
5757
}
5858

5959
static void write_file(const std::string & fname, const std::string & content) {
60-
std::ofstream file(fname);
60+
const std::string fname_tmp = fname + ".tmp";
61+
std::ofstream file(fname_tmp);
6162
if (!file) {
6263
throw std::runtime_error(string_format("error: failed to open file '%s'\n", fname.c_str()));
6364
}
6465
file << content;
6566
file.close();
67+
68+
// Makes write atomic
69+
if (rename(fname_tmp.c_str(), fname.c_str()) != 0) {
70+
LOG_ERR("%s: unable to rename file: %s to %s\n", __func__, fname_tmp.c_str(), fname.c_str());
71+
}
6672
}
6773

6874
common_arg & common_arg::set_examples(std::initializer_list<enum llama_example> examples) {
@@ -220,12 +226,25 @@ struct curl_slist_ptr {
220226
#define CURL_MAX_RETRY 3
221227
#define CURL_RETRY_DELAY_SECONDS 2
222228

223-
static bool curl_perform_with_retry(const std::string & url, CURL * curl, int max_attempts, int retry_delay_seconds, const char * method_name) {
229+
static bool curl_perform_with_retry(const std::string & url,
230+
CURL * curl,
231+
int max_attempts,
232+
int retry_delay_seconds,
233+
const char * method_name,
234+
const std::string & path_temporary = "") {
224235
int remaining_attempts = max_attempts;
225236

226237
while (remaining_attempts > 0) {
227238
LOG_INF("%s: %s %s (attempt %d of %d)...\n", __func__ , method_name, url.c_str(), max_attempts - remaining_attempts + 1, max_attempts);
228239

240+
if (std::filesystem::exists(path_temporary)) {
241+
const std::string partial_size = std::to_string(std::filesystem::file_size(path_temporary));
242+
LOG_INF("%s: server supports range requests, resuming download from byte %s\n", __func__,
243+
partial_size.c_str());
244+
const std::string range_str = partial_size + "-";
245+
curl_easy_setopt(curl, CURLOPT_RANGE, range_str.c_str());
246+
}
247+
229248
CURLcode res = curl_easy_perform(curl);
230249
if (res == CURLE_OK) {
231250
return true;
@@ -246,15 +265,14 @@ static bool curl_perform_with_retry(const std::string & url, CURL * curl, int ma
246265

247266
// download one single file from remote URL to local path
248267
static bool common_download_file_single(const std::string & url, const std::string & path, const std::string & bearer_token, bool offline) {
249-
// Check if the file already exists locally
250-
auto file_exists = std::filesystem::exists(path);
251-
252268
// If the file exists, check its JSON metadata companion file.
253269
std::string metadata_path = path + ".json";
254270
nlohmann::json metadata; // TODO @ngxson : get rid of this json, use regex instead
255271
std::string etag;
256272
std::string last_modified;
257273

274+
// Check if the file already exists locally
275+
const auto file_exists = std::filesystem::exists(path);
258276
if (file_exists) {
259277
if (offline) {
260278
LOG_INF("%s: using cached file (offline mode): %s\n", __func__, path.c_str());
@@ -289,6 +307,7 @@ static bool common_download_file_single(const std::string & url, const std::stri
289307
struct common_load_model_from_url_headers {
290308
std::string etag;
291309
std::string last_modified;
310+
std::string accept_ranges;
292311
};
293312

294313
common_load_model_from_url_headers headers;
@@ -328,7 +347,7 @@ static bool common_download_file_single(const std::string & url, const std::stri
328347
static std::regex header_regex("([^:]+): (.*)\r\n");
329348
static std::regex etag_regex("ETag", std::regex_constants::icase);
330349
static std::regex last_modified_regex("Last-Modified", std::regex_constants::icase);
331-
350+
static std::regex accept_ranges_regex("Accept-Ranges", std::regex_constants::icase);
332351
std::string header(buffer, n_items);
333352
std::smatch match;
334353
if (std::regex_match(header, match, header_regex)) {
@@ -338,6 +357,8 @@ static bool common_download_file_single(const std::string & url, const std::stri
338357
headers->etag = value;
339358
} else if (std::regex_match(key, match, last_modified_regex)) {
340359
headers->last_modified = value;
360+
} else if (std::regex_match(key, match, accept_ranges_regex)) {
361+
headers->accept_ranges = value;
341362
}
342363
}
343364
return n_items;
@@ -366,28 +387,48 @@ static bool common_download_file_single(const std::string & url, const std::stri
366387

367388
// if head_request_ok is false, we don't have the etag or last-modified headers
368389
// we leave should_download as-is, which is true if the file does not exist
390+
bool should_download_from_scratch = false;
369391
if (head_request_ok) {
370392
// check if ETag or Last-Modified headers are different
371393
// if it is, we need to download the file again
372394
if (!etag.empty() && etag != headers.etag) {
373395
LOG_WRN("%s: ETag header is different (%s != %s): triggering a new download\n", __func__, etag.c_str(), headers.etag.c_str());
374396
should_download = true;
397+
should_download_from_scratch = true;
375398
} else if (!last_modified.empty() && last_modified != headers.last_modified) {
376399
LOG_WRN("%s: Last-Modified header is different (%s != %s): triggering a new download\n", __func__, last_modified.c_str(), headers.last_modified.c_str());
377400
should_download = true;
401+
should_download_from_scratch = true;
378402
}
379403
}
380404

405+
const bool accept_ranges_supported = !headers.accept_ranges.empty() && headers.accept_ranges != "none";
381406
if (should_download) {
382-
std::string path_temporary = path + ".downloadInProgress";
383-
if (file_exists) {
407+
if (file_exists && !accept_ranges_supported) { // Resumable downloads not supported, delete and start again.
384408
LOG_WRN("%s: deleting previous downloaded file: %s\n", __func__, path.c_str());
385409
if (remove(path.c_str()) != 0) {
386410
LOG_ERR("%s: unable to delete file: %s\n", __func__, path.c_str());
387411
return false;
388412
}
389413
}
390414

415+
const std::string path_temporary = path + ".downloadInProgress";
416+
if (should_download_from_scratch) {
417+
if (std::filesystem::exists(path_temporary)) {
418+
if (remove(path_temporary.c_str()) != 0) {
419+
LOG_ERR("%s: unable to delete file: %s\n", __func__, path_temporary.c_str());
420+
return false;
421+
}
422+
}
423+
424+
if (std::filesystem::exists(path)) {
425+
if (remove(path.c_str()) != 0) {
426+
LOG_ERR("%s: unable to delete file: %s\n", __func__, path.c_str());
427+
return false;
428+
}
429+
}
430+
}
431+
391432
// Set the output file
392433

393434
struct FILE_deleter {
@@ -396,7 +437,8 @@ static bool common_download_file_single(const std::string & url, const std::stri
396437
}
397438
};
398439

399-
std::unique_ptr<FILE, FILE_deleter> outfile(fopen(path_temporary.c_str(), "wb"));
440+
// Always open file in append mode could be resuming
441+
std::unique_ptr<FILE, FILE_deleter> outfile(fopen(path_temporary.c_str(), "ab"));
400442
if (!outfile) {
401443
LOG_ERR("%s: error opening local file for writing: %s\n", __func__, path.c_str());
402444
return false;
@@ -431,7 +473,19 @@ static bool common_download_file_single(const std::string & url, const std::stri
431473
// start the download
432474
LOG_INF("%s: trying to download model from %s to %s (server_etag:%s, server_last_modified:%s)...\n", __func__,
433475
llama_download_hide_password_in_url(url).c_str(), path.c_str(), headers.etag.c_str(), headers.last_modified.c_str());
434-
bool was_perform_successful = curl_perform_with_retry(url, curl.get(), CURL_MAX_RETRY, CURL_RETRY_DELAY_SECONDS, "GET");
476+
477+
// Write the updated JSON metadata file.
478+
metadata.update({
479+
{"url", url},
480+
{"etag", headers.etag},
481+
{"lastModified", headers.last_modified}
482+
});
483+
write_file(metadata_path, metadata.dump(4));
484+
LOG_DBG("%s: file metadata saved: %s\n", __func__, metadata_path.c_str());
485+
486+
const bool was_perform_successful =
487+
curl_perform_with_retry(url, curl.get(), CURL_MAX_RETRY, CURL_RETRY_DELAY_SECONDS, "GET",
488+
headers.accept_ranges.empty() ? "" : path_temporary);
435489
if (!was_perform_successful) {
436490
return false;
437491
}
@@ -446,15 +500,6 @@ static bool common_download_file_single(const std::string & url, const std::stri
446500
// Causes file to be closed explicitly here before we rename it.
447501
outfile.reset();
448502

449-
// Write the updated JSON metadata file.
450-
metadata.update({
451-
{"url", url},
452-
{"etag", headers.etag},
453-
{"lastModified", headers.last_modified}
454-
});
455-
write_file(metadata_path, metadata.dump(4));
456-
LOG_DBG("%s: file metadata saved: %s\n", __func__, metadata_path.c_str());
457-
458503
if (rename(path_temporary.c_str(), path.c_str()) != 0) {
459504
LOG_ERR("%s: unable to rename file: %s to %s\n", __func__, path_temporary.c_str(), path.c_str());
460505
return false;
@@ -770,7 +815,7 @@ static std::string common_docker_get_token(const std::string & repo) {
770815
}
771816

772817
static std::string common_docker_resolve_model(const std::string & docker) {
773-
// Parse ai/smollm2:135M-Q4_K_M
818+
// Parse ai/smollm2:135M-Q4_0
774819
size_t colon_pos = docker.find(':');
775820
std::string repo, tag;
776821
if (colon_pos != std::string::npos) {

0 commit comments

Comments
 (0)