16 #include "spdlog/spdlog.h"
17 #include "spdlog/fmt/fmt.h"
18 #include "spdlog/fmt/chrono.h"
19 #include "nlohmann/json.hpp"
29 "DenseTXT",
"SparseTXT",
"DenseNPY",
"<NULL>"
34 false,
true,
false,
true
79 throw std::runtime_error(
"Invalid format");
94 std::fstream source(meta_file, std::fstream::in);
95 if(!source.is_open()) {
96 throw std::runtime_error(fmt::format(
"Could not open model metadata file '{}'", meta_file.c_str()));
101 getline (source, s,
'\0');
102 json meta = json::parse(s);
107 for(
auto& weight_file : meta[
"files"]) {
109 long count = weight_file[
"count"];
110 std::string weight_format = weight_file[
"weight-format"];
117 return std::lower_bound(begin(m_SubFiles), end(m_SubFiles), pos,
119 return s.
First < val;
125 return sf.First + (sf.Count - 1);
128 auto insert_pos = label_lower_bound(sub.
First);
133 if(insert_pos != m_SubFiles.end()) {
134 if(last_label(sub) >= insert_pos->First) {
135 throw std::logic_error(fmt::format(
"Overlap detected! Partial model in file {} stores weights {}-{}, "
136 "partial model in file {} stores {}-{}", insert_pos->FileName,
137 insert_pos->First.to_index(), last_label(*insert_pos).to_index(), sub.
FileName,
144 if(insert_pos != m_SubFiles.begin()) {
145 const auto& prev_el = *std::prev(insert_pos);
146 if(last_label(prev_el) >= sub.
First) {
147 throw std::logic_error(fmt::format(
"Overlap detected! Partial model in file {} stores weights {}-{}, "
148 "partial model in file {} stores {}-{}", prev_el.FileName, prev_el.First.to_index(),
153 m_SubFiles.insert(insert_pos, sub);
161 m_Options(options), m_MetaFileName(std::move(target_file)) {
167 throw std::runtime_error(fmt::format(
"Cannot save to '{}' because directory does not exist.",
182 throw std::logic_error(fmt::format(
"Received partial model for {} labels, but expected {} labels",
186 throw std::logic_error(fmt::format(
"Received partial model for {} features, but expected {} features",
192 if(!file_path.has_value()) {
193 target_file.replace_filename(fmt::format(
"{}.weights-{}-{}",
m_MetaFileName.filename().c_str(),
194 model->labels_begin().to_index(), model->labels_end().to_index() - 1));
204 return std::async(std::launch::deferred, [new_weights_file](){
205 return new_weights_file;
209 using namespace std::chrono;
210 std::fstream weights_file(target_file, std::fstream::out | std::fstream::binary);
211 if(!weights_file.is_open()) {
212 throw std::runtime_error(fmt::format(
"Could not create weights file {}", target_file.c_str()));
220 return std::async(std::launch::async, [
this, target=std::move(weights_file), model, new_weights_file]()
mutable
225 auto now = steady_clock::now();
227 spdlog::info(
"Saving partial model for weights {}-{} took {} ms",
228 model->labels_begin().to_index(), model->labels_begin().to_index() + model->num_weights(),
229 duration_cast<milliseconds>(steady_clock::now() - now).count());
230 return new_weights_file;
238 std::time_t tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
239 std::tm tm = *std::gmtime(&tt);
240 char date_buffer[128];
241 strftime(date_buffer,
sizeof(date_buffer),
"%F - %T", &tm);
242 meta[
"date"] = date_buffer;
246 json file_data = {{
"first", sub.First.to_index()}, {
"count", sub.Count},
247 {
"file", sub.FileName}, {
"weight-format",
to_string(sub.Format)}};
248 meta[
"files"].push_back(file_data);
252 meta_file << std::setw(4) << meta <<
"\n";
261 if(end > iter->First) {
268 const auto& prev_el = *std::prev(iter);
269 if(prev_el.First + prev_el.Count > begin) {
280 if(last_end != sub.First) {
281 throw std::logic_error(fmt::format(
"Some labels are missing. Gap from {} to {}", last_end.to_index(), sub.First.to_index() - 1));
283 last_end = sub.First + sub.Count;
286 throw std::logic_error(fmt::format(
"Some labels are missing. Gap from {} to {}", last_end.to_index(),
m_TotalLabels - 1));
296 if(last_end != sub.First) {
297 return {last_end, sub.First};
299 last_end = sub.First + sub.Count;
301 if(last_end != label_end) {
302 return {last_end, label_end};
305 return {label_end, label_end};
310 if(model->is_partial_model()) {
311 throw std::logic_error(
"save_model can only save complete models");
317 if(model->num_labels() < options.
SplitFiles) {
318 saver.add_model(model, fmt::format(
"{}.weights", target_file.filename().c_str()));
320 int num_files = std::round( model->num_weights() /
static_cast<double>(options.
SplitFiles) );
321 for(
int sub = 0; sub < num_files; ++sub) {
322 std::string weights_file_name = fmt::format(
"{}.weights-{}-of-{}", target_file.filename().c_str(), 1+sub, num_files);
324 long end_label = (sub == num_files - 1) ? model->num_weights() : (sub+1) * options.
SplitFiles;
325 end_label = std::min(end_label, model->num_labels());
326 auto submodel = std::make_shared<::model::ConstSubModelView>(model.get(), first,
label_id_t{end_label});
327 saver.add_model(submodel, weights_file_name);
349 auto sub_files = std::equal_range(begin(m_SubFiles), end(m_SubFiles),
352 auto a1 = left.
First;
353 auto a2 = right.First;
354 auto b1 = a1 + left.
Count;
357 if(sub_files.first == end(m_SubFiles)) {
358 throw std::runtime_error(fmt::format(
"Could not find weights for interval [{}, {})",
359 label_begin.to_index(), label_end.to_index()));
362 auto calc_label_count = [&](){
363 if(sub_files.second == end(m_SubFiles)) {
366 return sub_files.second->First;
370 return SubModelRangeSpec{sub_files.first, sub_files.second, sub_files.first->First, calc_label_count()};
379 auto model = std::make_shared<::model::DenseModel>(
m_NumFeatures, spec);
381 for(
auto file = sub_range.FilesBegin; file < sub_range.FilesEnd; ++file) {
384 std::fstream source(weights_file.replace_filename(file->FileName), std::fstream::in);
385 if(!source.is_open()) {
386 THROW_ERROR(
"Could not open weights file ", weights_file.replace_filename(file->FileName).string());
389 spdlog::info(
"read weight file {}", weights_file.replace_filename(file->FileName).c_str());
402 return std::make_shared<::model::SparseModel>(num_features, spec);
404 return std::make_shared<::model::DenseModel>(num_features, spec);
418 __builtin_unreachable();
424 auto start = std::chrono::steady_clock::now();
430 std::fstream source(weights_file.replace_filename(entry.
FileName), std::fstream::in);
431 if(!source.is_open()) {
432 THROW_ERROR(
"Could not open weights file '{}'", weights_file.replace_filename(entry.
FileName).string());
435 auto duration = std::chrono::steady_clock::now() - start;
436 spdlog::info(
"read weight file '{}' in {}ms", weights_file.replace_filename(entry.
FileName).c_str(),
437 std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
445 auto wf = weights_file.replace_filename(entry.FileName);
448 if(!std::filesystem::exists(wf)) {
449 spdlog::error(
"Weight file '{}' does not exist!", wf.string());
454 std::fstream source(weights_file.replace_filename(entry.FileName), std::fstream::in);
455 if(!source.is_open()) {
456 spdlog::error(
"Could not open weight file '{}'", wf.string());
466 using ::model::DenseModel;
467 using ::model::PartialModelSpec;
481 std::filesystem::create_directory(
"test");
484 auto first_part = std::make_shared<DenseModel>(4, PartialModelSpec{
label_id_t{1}, 4, 20});
486 REQUIRE_NOTHROW(pms.
add_model(first_part));
488 SUBCASE(
"mismatched features") {
490 auto mismatched_features = std::make_shared<DenseModel>(7, spec);
491 auto valid = std::make_shared<DenseModel>(4, spec);
492 REQUIRE_THROWS(pms.
add_model(mismatched_features));
496 SUBCASE(
"mismatched total labels") {
497 auto mismatched_labels = std::make_shared<DenseModel>(4, PartialModelSpec{
label_id_t{5}, 2, 50});
498 auto valid = std::make_shared<DenseModel>(4, PartialModelSpec{
label_id_t{5}, 2, 20});
499 REQUIRE_THROWS(pms.
add_model(mismatched_labels));
503 SUBCASE(
"incomplete model") {
519 CHECK(tm.label_lower_bound(
label_id_t{0}) == tm.m_SubFiles.begin());
520 CHECK(tm.label_lower_bound(
label_id_t{20}) == tm.m_SubFiles.begin());
521 CHECK(tm.label_lower_bound(
label_id_t{40}) == tm.m_SubFiles.begin() + 1);
522 CHECK(tm.label_lower_bound(
label_id_t{50}) == tm.m_SubFiles.begin() + 1);
523 CHECK(tm.label_lower_bound(
label_id_t{80}) == tm.m_SubFiles.begin() + 1);
524 CHECK(tm.label_lower_bound(
label_id_t{100}) == tm.m_SubFiles.begin() + 1);
525 CHECK(tm.label_lower_bound(
label_id_t{120}) == tm.m_SubFiles.end());
526 CHECK(tm.label_lower_bound(
label_id_t{200}) == tm.m_SubFiles.end());
531 TestModel() =
default;
532 ~TestModel() =
default;
533 void insert(
long first,
long count) {
540 REQUIRE_NOTHROW(pms.insert(1, 4));
543 SUBCASE(
"overlap predecessor") {
544 REQUIRE_THROWS(pms.insert(4, 2));
545 CHECK_NOTHROW(pms.insert(5, 2));
548 SUBCASE(
"overlap successor") {
549 REQUIRE_THROWS(pms.insert(0, 2));
550 CHECK_NOTHROW(pms.insert(0, 1));
This class is used as an implementation detail to capture the common code of PartialModelSaver and Pa...
weight_file_iter_t label_lower_bound(label_id_t pos) const
Gets an iterator into the weight-file list that points to the first element whose starting label is l...
void read_metadata_file(const path &meta_file)
void insert_sub_file(const WeightFileEntry &data)
Inserts a new sub-file entry into the metadata object.
std::vector< WeightFileEntry > m_SubFiles
long num_labels() const noexcept
Gets the total number of labels.
long num_features() const noexcept
Gets the total number of features.
This class allows loading only a subset of the weights of a large model.
std::shared_ptr< Model > load_model(label_id_t label_begin, label_id_t label_end) const
Loads part of the model.
long num_weight_files() const
Returns the number of availabel weight files.
bool validate() const
Validates that all weight files exist.
const path & meta_file_path() const
The path to the metadata file.
SubModelRangeSpec get_loading_range(label_id_t label_begin, label_id_t label_end) const
PartialModelLoader(path meta_file, ESparseMode mode=DEFAULT)
Create a new PartialModelLoader for the given metadata file.
Manage saving a model consisting of multiple partial models.
void finalize()
Checks that all weights have been written and updates the metadata file.
PartialModelSaver(path target_file, SaveOption options, bool load_partial=false)
Create a new PartialModelSaver.
bool any_weight_vector_for_interval(label_id_t begin, label_id_t end) const
Checks if there are any weight vectors for the given interval.
std::pair< label_id_t, label_id_t > get_missing_weights() const
Get an interval labels for which weights are missing.
std::future< WeightFileEntry > add_model(const std::shared_ptr< const Model > &model, const std::optional< std::string > &file_path={})
Adds the weights of a partial model asynchronously.
void insert_sub_file(const WeightFileEntry &data)
Inserts a new sub-file entry into the metadata object.
void update_meta_file()
Updates the metadata file.
Strong typedef for an int to signify a label id.
A model combines a set of weight with some meta-information about these weights.
constexpr T to_index() const
! Explicitly convert to an integer.
building blocks for io procedures that are used by multiple io subsystems
TEST_CASE("partial model writer verifier")
void save_weights_dispatch(std::ostream &target, const Model &model, SaveOption &options)
This function calls on of the save functions, depending on the format specified on options
void read_weights_dispatch(std::istream &source, WeightFormat format, Model &model)
This function calls the reads function that corresponds to the given weight format.
const std::array< const char *, 4 > weight_format_names
Translation from io::model::WeightFormat to std::string.
const std::array< bool, 4 > weight_format_sparsity
Lookup which mode has sparse weights.
std::shared_ptr< Model > make_model(long num_features, ::model::PartialModelSpec spec, bool sparse)
bool use_sparse_weights(PartialModelLoader::ESparseMode mode, WeightFormat format)
namespace for all model-related io functions.
void load_sparse_weights_txt(std::istream &source, Model &target)
Loads sparse weights from plain-text format.
std::shared_ptr< Model > load_model(path source)
WeightFormat parse_weights_format(std::string_view name)
Gets the eighs.
void save_dense_weights_npy(std::streambuf &target, const Model &model)
Saves the dense weights in a npy file.
void save_model(const path &target_file, const std::shared_ptr< const Model > &model, SaveOption options)
Saves a complete model to a file.
void save_as_sparse_weights_txt(std::ostream &target, const Model &model, double threshold)
Saves the weights in sparse plain-text format, culling small weights.
WeightFormat
Describes the format in which the weight data has been saved.
@ DENSE_TXT
Dense Text Format
@ SPARSE_TXT
Sparse Text Format
@ DENSE_NPY
Dense Numpy Format
const char * to_string(WeightFormat format)
void load_dense_weights_txt(std::istream &source, Model &target)
Loads weights saved by io::model::save_dense_weights_txt.
void save_dense_weights_txt(std::ostream &target, const Model &model)
Saves the dense weights in a plain-text format.
void load_dense_weights_npy(std::streambuf &target, Model &model)
Loads dense weights from a npy file.
void pin_to_data(const void *data)
Pint the calling thread to the NUMA node on which data resides.
Main namespace in which all types, classes, and functions are defined.
WeightFormat Format
Format in which the weights will be saved.
double Culling
If saving in sparse mode, threshold below which weights will be omitted.
int Precision
Precision with which the labels will be saved.
int SplitFiles
Maximum number of weight vectors per file.
Collect the data about a weight file.
Specifies how to interpret a weight matrix for a partial model.