diff --git a/AGENTS.md b/AGENTS.md index 413423b..f131286 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,7 @@ src/ libparakeet implementation audio_io.hpp/cpp , dr_wav load + linear resample to 16k model_loader.hpp/cpp, GGUF -> ParakeetConfig + name->tensor bundle.hpp/cpp , bundle GGUF header: components, licences, default component (docs/bundle.md) - bundle_extract.hpp/cpp, one bundle component as a standalone GGUF (memfd or temporary file) for the path-only ced.cpp and voice-detect.cpp loaders + bundle_map.hpp/cpp, read-only map of a bundle handed to the load-from-memory hooks of ced.cpp and voice-detect.cpp (no temporary file) mel.cpp , log-mel frontend encoder.cpp / conformer.cpp / relpos_attention.cpp ctc_decoder.cpp , CTC head + greedy decode @@ -134,7 +134,7 @@ tests/ ctest targets test_model_loader.cpp , config + tensor map (model-dependent) test_bundle.cpp , bundle header, selection, partial read, Silero component, C-API (model-independent, synthetic files) test_bundle_models.cpp , real bundle == single-model files: transcript, VAD head, Silero (PARAKEET_TEST_BUNDLE, _ASR, _SILERO) - test_bundle_full.cpp , full bundle (asr, diar, ced, voice, vad) == single-model files, memfd and temporary-file paths (PARAKEET_TEST_BUNDLE_FULL, PARAKEET_TEST_FULL_*) + test_bundle_full.cpp , full bundle (asr, diar, ced, voice, vad) == single-model files, no temporary file or descriptor, storage bytes read per component (PARAKEET_TEST_BUNDLE_FULL, PARAKEET_TEST_FULL_*) python/check_bundle.py , bundle_gguf.py build/verify/refusals, licence policy, NOTICE (model-independent) test_capi.cpp , C-API load -> transcribe -> free (model-dependent) test_transcribe_speech.cpp, end-to-end CTC transcript (model-dependent) diff --git a/CMakeLists.txt b/CMakeLists.txt index 27dc107..6b900f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,7 +112,7 @@ set(PARAKEET_SRC src/vad_segmenter.cpp src/vad_json.cpp src/bundle.cpp - src/bundle_extract.cpp + src/bundle_map.cpp src/parakeet_capi.cpp src/common.cpp src/audio_io.cpp diff --git a/docs/bundle.md b/docs/bundle.md index a2dba41..0a1bb76 100644 --- a/docs/bundle.md +++ b/docs/bundle.md @@ -119,34 +119,69 @@ worth knowing: already has the head), and `--vad-component` that names a slice is an error. | `diar` | A Nemotron diarization GGUF (`parakeet.arch` `diarization`) | `DiarizationModel::load(path, name)`, through the same prefixed loader as `asr` | -| `ced` | A ced.cpp GGUF (`general.architecture` `ced`, CED-tiny, -mini, -small or -base) | `CedTagger::load(path, name)`, through a standalone copy (see "Components for third-party loaders") | -| `voice` | A voice-detect.cpp speaker encoder GGUF (`general.architecture` `voicedetect` with an embedding: WeSpeaker ResNet34, ECAPA, ERes2Net, CAM++) | `SpeakerEncoder::load(path, name)`, through a standalone copy | +| `ced` | A ced.cpp GGUF (`general.architecture` `ced`, CED-tiny, -mini, -small or -base) | `CedTagger::load(path, name)`, from a read-only map of the bundle (see "ced and voice components") | +| `voice` | A voice-detect.cpp speaker encoder GGUF (`general.architecture` `voicedetect` with an embedding: WeSpeaker ResNet34, ECAPA, ERes2Net, CAM++) | `SpeakerEncoder::load(path, name)`, from a read-only map of the bundle | A reader skips components of a kind it does not know: they are listed, but it does not load them and does not fail because they exist. The wav2vec2 analysis heads of voice-detect.cpp (age, gender, emotion) are not a `voice` component: their licence is non-commercial, and the build script refuses them. -## Components for third-party loaders - -`ced.cpp` and `voice-detect.cpp` open a model by file path only, and parakeet.cpp -does not change their sources. To load a `ced` or `voice` component, parakeet.cpp -writes it as a standalone single-model GGUF (keys and tensors without the prefix, -data streamed from the bundle, only that component's bytes read) and gives the -loader the path of that copy: - -* **Linux:** an anonymous in-memory file (`memfd_create`), opened as - `/proc/self/fd/N`. Nothing is written to disk. The loader reads it, then the - file is closed. The copy briefly uses memory equal to the component size. -* **Other systems, or if `memfd_create` or `/proc` is not usable, or when the - environment variable `PARAKEET_BUNDLE_NO_MEMFD` is set to a value other than - `0`:** a temporary file in the system temporary directory (`TMPDIR`, `TEMP`, - else `/tmp`), created owner-only with an exclusive create, and removed as soon as - the load has finished. A crash during the load can leave the file behind. The - path is only used on macOS and Windows by this fallback; it is not tested there. +## ced and voice components + +`ced.cpp` and `voice-detect.cpp` can load a model from memory: +`ced_capi_load_from_memory_prefixed(data, size, prefix)` and +`voicedetect_capi_load_from_memory_prefixed(data, size, prefix)`. They take the +bytes of a whole GGUF that holds the model under a prefix (`ced.`, `voice.`, the +component name and a dot), parse its header, and copy only the tensors of that +prefix into memory of their own. parakeet.cpp uses them as follows: + +1. It reads the bundle header and checks that the component exists and has the + right kind. +2. It maps the bundle file read-only (`mmap` on Linux and macOS, + `MapViewOfFile` on Windows; `src/bundle_map.cpp`). A map reads nothing by + itself: the operating system loads only the pages that are touched. +3. It calls the loader with the map and the prefix. The loader touches the header + and the tensors of the component, copies them, and returns. The map is + released at once. The loader keeps no pointer into it. + +There is no standalone copy of the component, no temporary file, no memory file +(`memfd`), and no environment variable. The bundle file is opened read-only for +the moment it takes to map it, and closed again. Linux, macOS and Windows run the +same code; only Linux is run in the tests and in CI here. + +**Memory and I/O.** The other components are never read. Measured with the +published standard bundle (1100.8 MB: asr 940.5 MB, diar 108.6 MB, ced-small Q8_0 +23.6 MB, voice WeSpeaker ResNet34 F32 26.5 MB, vad), loading one component in a +fresh process: + +| Component | Component size | Peak resident memory above an idle process | Memory kept after the load | Storage read (page cache dropped before) | +| --- | --- | --- | --- | --- | +| `ced` | 23.6 MB | 49 MB | 25 MB | 24.2 MB | +| `voice` | 26.5 MB | 54 MB | 27 MB | 26.5 MB | + +The peak is the copy of the component (anonymous memory, kept) plus the pages of +the map that were touched (file cache, reclaimable, released with the map). It +depends on the size of the component and not on the size of the bundle: the 338 MB +small bundle gives the same figures. The previous design wrote the component to an +in-memory file first, so it held one more copy of the component while the loader ran (not measured). + +**Limits.** + +* A model file must not change while it loads. If another process shortens the + bundle while it is mapped, a read past the new end ends the process + (`SIGBUS`, or an access violation on Windows). Model files are meant to be + immutable in service. +* The whole bundle needs address space, not memory. A 32-bit process cannot map a + bundle larger than its address space; the load then fails with "cannot map". +* A tensor name in the bundle is `.`, and ggml keeps names in 63 + bytes, so the stripped names the loaders see are shorter than that. The component loaded this way gives the same output as the single-model file: the CED class scores and the speaker embeddings are bitwise equal in the tests. +Load errors come from `ced_capi_last_error(NULL)` and +`voicedetect_capi_last_load_error()` and are added to the message of +`CedTagger::load` and `SpeakerEncoder::load`. The speaker identity of a `voice` component (`parakeet_capi_speaker_identity`) is `sha256:` plus the `source_sha256` of its header, which is the sha256 of the @@ -154,12 +189,6 @@ single-model file. A voice enrolled with the standalone file therefore matches t same model inside a bundle. The header is trusted for this: load bundles you trust. -Cleaner alternative, a follow-up outside this repository: a `load_from_buffer` -(or `load_from_reader`) entry point in ced.cpp and voice-detect.cpp, so a -component could be handed over as a memory range with no copy and no temporary -file. Both projects are owned by the same maintainer. When it exists, -`bundle_extract.cpp` can be replaced by a call that passes the mapped range. - ## Compatibility rules 1. **Plain files are unchanged.** A single-model GGUF has no bundle keys. Every @@ -217,16 +246,16 @@ A component is chosen by name or by default. ## Partial loading A loader reads the header and the tensor table, then reads only the tensors of -the component it was asked for. The ASR and -diarization loaders read them into one private memory block, and the Silero loader -seeks to each of its tensors. The `ced` and `voice` loaders read them while they -write the standalone copy, then read that copy. The bytes of the other components -are never read. A test checks the number of bytes read from -the file for each component. +the component it was asked for. The ASR and diarization loaders read them into +one private memory block, and the Silero loader seeks to each of its tensors. The +`ced` and `voice` loaders get a read-only map of the bundle and copy the tensors +of their component out of it (see "ced and voice components"). The bytes of the +other components are never read. A test checks the bytes read from storage for +each component. Memory: a loaded component uses the same memory as the same single-model file. -The memory of components that are not loaded is not used. The file stays on disk -and is not memory mapped. +The memory of components that are not loaded is not used. For `ced` and `voice` +the map exists only during the load. ## Building, inspecting and verifying diff --git a/src/bundle_extract.cpp b/src/bundle_extract.cpp deleted file mode 100644 index d360449..0000000 --- a/src/bundle_extract.cpp +++ /dev/null @@ -1,252 +0,0 @@ -#include "bundle_extract.hpp" - -#include -#include -#include -#include -#include -#include - -#include "ggml.h" -#include "gguf.h" - -#if defined(__linux__) -#include -#include -#include -#include -#elif !defined(_WIN32) -#include -#include -#include -#endif - -namespace pk { - -namespace { - -#if defined(_WIN32) -typedef long long off_t_compat; -#define PK_FSEEK _fseeki64 -#define PK_FTELL _ftelli64 -#else -typedef off_t off_t_compat; -#define PK_FSEEK fseeko -#define PK_FTELL ftello -#endif - -void set_err(std::string* err, const std::string& m) { - if (err) *err = m; -} - -// Copies one key of `g` to `out` under `name`. Unknown value types are skipped. -void copy_kv(gguf_context* out, gguf_context* g, int64_t i, const std::string& name) { - const char* k = name.c_str(); - switch (gguf_get_kv_type(g, i)) { - case GGUF_TYPE_UINT8: gguf_set_val_u8(out, k, gguf_get_val_u8(g, i)); break; - case GGUF_TYPE_INT8: gguf_set_val_i8(out, k, gguf_get_val_i8(g, i)); break; - case GGUF_TYPE_UINT16: gguf_set_val_u16(out, k, gguf_get_val_u16(g, i)); break; - case GGUF_TYPE_INT16: gguf_set_val_i16(out, k, gguf_get_val_i16(g, i)); break; - case GGUF_TYPE_UINT32: gguf_set_val_u32(out, k, gguf_get_val_u32(g, i)); break; - case GGUF_TYPE_INT32: gguf_set_val_i32(out, k, gguf_get_val_i32(g, i)); break; - case GGUF_TYPE_FLOAT32: gguf_set_val_f32(out, k, gguf_get_val_f32(g, i)); break; - case GGUF_TYPE_UINT64: gguf_set_val_u64(out, k, gguf_get_val_u64(g, i)); break; - case GGUF_TYPE_INT64: gguf_set_val_i64(out, k, gguf_get_val_i64(g, i)); break; - case GGUF_TYPE_FLOAT64: gguf_set_val_f64(out, k, gguf_get_val_f64(g, i)); break; - case GGUF_TYPE_BOOL: gguf_set_val_bool(out, k, gguf_get_val_bool(g, i)); break; - case GGUF_TYPE_STRING: gguf_set_val_str(out, k, gguf_get_val_str(g, i)); break; - case GGUF_TYPE_ARRAY: { - const gguf_type at = gguf_get_arr_type(g, i); - const size_t n = gguf_get_arr_n(g, i); - if (at == GGUF_TYPE_STRING) { - std::vector v(n); - for (size_t j = 0; j < n; ++j) v[j] = gguf_get_arr_str(g, i, j); - gguf_set_arr_str(out, k, v.data(), n); - } else if (at != GGUF_TYPE_ARRAY) { - gguf_set_arr_data(out, k, at, gguf_get_arr_data(g, i), n); - } - break; - } - default: break; - } -} - -bool no_memfd_requested() { - const char* e = std::getenv("PARAKEET_BUNDLE_NO_MEMFD"); - return e && *e && std::strcmp(e, "0") != 0; -} - -// Writes the standalone GGUF of `comp` to `f` (a freshly opened, empty file). -// Returns the number of bytes written, or 0 with *err set. -uint64_t write_component(const std::string& bundle, const std::string& comp, FILE* f, std::string* err) { - ggml_context* meta = nullptr; - gguf_init_params ip{/*no_alloc*/ true, &meta}; - gguf_context* g = gguf_init_from_file(bundle.c_str(), ip); - if (!g) { - set_err(err, "cannot read " + bundle + " as a GGUF file"); - return 0; - } - const std::string pre = comp + "."; - gguf_context* out = gguf_init_empty(); - ggml_context* tctx = nullptr; - FILE* in = nullptr; - uint64_t written = 0; - bool ok = false; - do { - for (int64_t i = 0; i < gguf_get_n_kv(g); ++i) { - const char* k = gguf_get_key(g, i); - if (std::strncmp(k, pre.c_str(), pre.size()) != 0) continue; - copy_kv(out, g, i, std::string(k + pre.size())); - } - const int64_t nt = gguf_get_n_tensors(g); - std::vector ids; - for (int64_t i = 0; i < nt; ++i) - if (std::strncmp(gguf_get_tensor_name(g, i), pre.c_str(), pre.size()) == 0) ids.push_back(i); - if (ids.empty()) { - set_err(err, "bundle " + bundle + ": component '" + comp + "' has no tensors"); - break; - } - // Tensor descriptors only (no data): the writer needs type, shape and name. - ggml_init_params tp{ggml_tensor_overhead() * ids.size() + (1u << 16), nullptr, /*no_alloc*/ true}; - tctx = ggml_init(tp); - if (!tctx) { set_err(err, "out of memory"); break; } - bool bad = false; - for (int64_t i : ids) { - const char* nm = gguf_get_tensor_name(g, i); - ggml_tensor* s = ggml_get_tensor(meta, nm); - ggml_tensor* t = s ? ggml_new_tensor(tctx, s->type, GGML_MAX_DIMS, s->ne) : nullptr; - if (!t) { set_err(err, std::string("cannot describe tensor ") + nm); bad = true; break; } - ggml_set_name(t, nm + pre.size()); - gguf_add_tensor(out, t); - } - if (bad) break; - if (!gguf_write_to_file_ptr(out, f, /*only_meta*/ true)) { set_err(err, "cannot write the component file"); break; } - written = (uint64_t)gguf_get_data_offset(out); // header + tensor table, padded - in = std::fopen(bundle.c_str(), "rb"); - if (!in) { set_err(err, "cannot open " + bundle); break; } - PK_FSEEK(in, 0, SEEK_END); - const uint64_t file_size = (uint64_t)PK_FTELL(in); - const uint64_t base = (uint64_t)gguf_get_data_offset(g); - std::vector buf(1u << 20); - bool failed = false; - for (size_t n = 0; n < ids.size() && !failed; ++n) { - const int64_t i = ids[n]; - const uint64_t size = gguf_get_tensor_size(g, i); - const uint64_t off = base + gguf_get_tensor_offset(g, i); - if (off + size > file_size) { - set_err(err, std::string("bundle ") + bundle + " is truncated (tensor " + gguf_get_tensor_name(g, i) + ")"); - failed = true; - break; - } - // Pad up to this tensor's aligned offset in the output. - const uint64_t want = (uint64_t)gguf_get_data_offset(out) + gguf_get_tensor_offset(out, (int64_t)n); - while (written < want) { - const char zero[64] = {0}; - const size_t k = (size_t)std::min(sizeof zero, want - written); - if (std::fwrite(zero, 1, k, f) != k) { failed = true; break; } - written += k; - } - if (failed || PK_FSEEK(in, (off_t_compat)off, SEEK_SET) != 0) { failed = true; break; } - for (uint64_t done = 0; done < size && !failed;) { - const size_t k = (size_t)std::min(buf.size(), size - done); - if (std::fread(buf.data(), 1, k, in) != k || std::fwrite(buf.data(), 1, k, f) != k) failed = true; - done += k; - written += k; - } - } - if (failed) { - if (!err || err->empty()) set_err(err, "cannot copy the component data (disk or memory full?)"); - break; - } - // Pad the end to the alignment like the single-pass writer does. - const size_t al = gguf_get_alignment(out); - while (written % al) { - if (std::fputc(0, f) == EOF) { failed = true; break; } - ++written; - } - if (failed || std::fflush(f) != 0) { set_err(err, "cannot write the component file"); break; } - ok = true; - } while (false); - if (in) std::fclose(in); - if (tctx) ggml_free(tctx); - gguf_free(out); - gguf_free(g); - if (meta) ggml_free(meta); - return ok ? written : 0; -} - -} // namespace - -ComponentFile::~ComponentFile() { -#if !defined(_WIN32) - if (fd_ >= 0) ::close(fd_); -#endif - if (remove_) { - std::error_code ec; - std::filesystem::remove(path_, ec); - } -} - -std::unique_ptr ComponentFile::create(const std::string& bundle, const std::string& comp, - std::string* err) { - std::unique_ptr cf(new ComponentFile()); -#if defined(__linux__) - if (!no_memfd_requested()) { - const int fd = (int)memfd_create("parakeet-component", MFD_CLOEXEC); - if (fd >= 0) { - const std::string p = "/proc/self/fd/" + std::to_string(fd); - FILE* f = ::fdopen(::dup(fd), "wb"); - std::string e; - const uint64_t n = f ? write_component(bundle, comp, f, &e) : 0; - if (f) std::fclose(f); - struct stat st {}; - if (n > 0 && ::stat(p.c_str(), &st) == 0) { // /proc must be mounted for the path to work - cf->fd_ = fd; - cf->path_ = p; - cf->in_memory_ = true; - cf->size_ = n; - return cf; - } - ::close(fd); - // A real failure (missing component, truncated bundle) is final; only fall back - // when the in-memory file itself could not be used. - if (n == 0 && !e.empty() && e.find("cannot write") == std::string::npos && - e.find("cannot copy") == std::string::npos) { - set_err(err, e); - return nullptr; - } - } - } -#endif - // Temporary file. - std::error_code ec; - const std::filesystem::path dir = std::filesystem::temp_directory_path(ec); - if (ec) { set_err(err, "no temporary directory for the component file"); return nullptr; } - FILE* f = nullptr; - std::string path; -#if defined(_WIN32) - for (int attempt = 0; attempt < 100 && !f; ++attempt) { - path = (dir / ("parakeet-component-" + std::to_string(std::rand()) + "-" + std::to_string(attempt) + ".gguf")).string(); - if (std::filesystem::exists(path)) continue; - f = std::fopen(path.c_str(), "wb"); - } -#else - for (int attempt = 0; attempt < 100 && !f; ++attempt) { - path = (dir / ("parakeet-component-" + std::to_string((long)::getpid()) + "-" + std::to_string(std::rand()) + - "-" + std::to_string(attempt) + ".gguf")).string(); - const int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0600); // never follow or reuse a file - if (fd >= 0) f = ::fdopen(fd, "wb"); - } -#endif - if (!f) { set_err(err, "cannot create a temporary file in " + dir.string()); return nullptr; } - cf->path_ = path; - cf->remove_ = true; // from here the destructor deletes the file, also on failure - std::string e; - const uint64_t n = write_component(bundle, comp, f, &e); - std::fclose(f); - if (n == 0) { set_err(err, e.empty() ? "cannot write the temporary file " + path : e); return nullptr; } - cf->size_ = n; - return cf; -} - -} // namespace pk diff --git a/src/bundle_extract.hpp b/src/bundle_extract.hpp deleted file mode 100644 index 829885b..0000000 --- a/src/bundle_extract.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -// A standalone single-model GGUF made from one component of a bundle, for a -// third-party loader that only opens a file by path (ced.cpp, voice-detect.cpp). -// -// The component's keys and tensors are written without the "." prefix, -// so the result is a valid single-model GGUF that the loader reads as if it had -// been published on its own. Tensor data is streamed from the bundle in blocks; -// only the requested component's bytes are read. -// -// Where the file lives: -// * Linux: an anonymous in-memory file (memfd_create), opened through -// /proc/self/fd/N. Nothing is written to disk. -// * Elsewhere, or when memfd_create or /proc is not available, or when the -// environment variable PARAKEET_BUNDLE_NO_MEMFD is set to a value other than -// "0": a temporary file in the system temporary directory (TMPDIR, TEMP, -// else /tmp), created owner-only and removed when the ComponentFile is -// destroyed. A crash between creation and removal leaves the file behind. -// -// Keep the object alive until the loader has finished opening the file, then -// let it go out of scope. -#include -#include -#include - -namespace pk { - -class ComponentFile { -public: - // Returns nullptr and writes the reason to *err on failure (bundle unreadable, - // component missing, truncated bundle, no space for the file). - static std::unique_ptr create(const std::string& bundle, const std::string& component, - std::string* err); - ~ComponentFile(); - ComponentFile(const ComponentFile&) = delete; - ComponentFile& operator=(const ComponentFile&) = delete; - - // Path to pass to a path-only loader. Valid until the object is destroyed. - const std::string& path() const { return path_; } - // True when the file is an in-memory file, false for a temporary file on disk. - bool in_memory() const { return in_memory_; } - // Size of the standalone GGUF in bytes. - uint64_t size() const { return size_; } - -private: - ComponentFile() = default; - std::string path_; - bool in_memory_ = false; - int fd_ = -1; // memfd, when in_memory_ - bool remove_ = false; // temporary file to delete - uint64_t size_ = 0; -}; - -} // namespace pk diff --git a/src/bundle_map.cpp b/src/bundle_map.cpp new file mode 100644 index 0000000..954f54a --- /dev/null +++ b/src/bundle_map.cpp @@ -0,0 +1,80 @@ +#include "bundle_map.hpp" + +#include +#include + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#include +#include +#include +#endif + +namespace pk { + +namespace { +void set_err(std::string* err, const std::string& m) { + if (err) *err = m; +} +} // namespace + +#if defined(_WIN32) + +MappedFile::~MappedFile() { + if (data_) UnmapViewOfFile(data_); + if (mapping_) CloseHandle(static_cast(mapping_)); +} + +std::unique_ptr MappedFile::open(const std::string& path, std::string* err) { + HANDLE f = CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (f == INVALID_HANDLE_VALUE) { set_err(err, "cannot open " + path); return nullptr; } + LARGE_INTEGER sz; + if (!GetFileSizeEx(f, &sz) || sz.QuadPart <= 0 || (uint64_t)sz.QuadPart > std::numeric_limits::max()) { + CloseHandle(f); + set_err(err, "cannot map " + path + " (empty or too large)"); + return nullptr; + } + std::unique_ptr m(new MappedFile()); + HANDLE h = CreateFileMappingA(f, nullptr, PAGE_READONLY, 0, 0, nullptr); + CloseHandle(f); // the mapping keeps the file open + if (!h) { set_err(err, "cannot map " + path); return nullptr; } + m->mapping_ = h; + m->data_ = MapViewOfFile(h, FILE_MAP_READ, 0, 0, 0); + if (!m->data_) { set_err(err, "cannot map " + path); return nullptr; } + m->size_ = (size_t)sz.QuadPart; + return m; +} + +#else + +MappedFile::~MappedFile() { + if (data_) ::munmap(const_cast(data_), size_); +} + +std::unique_ptr MappedFile::open(const std::string& path, std::string* err) { + const int fd = ::open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { set_err(err, "cannot open " + path); return nullptr; } + struct stat st {}; + if (::fstat(fd, &st) != 0 || st.st_size <= 0 || (uint64_t)st.st_size > std::numeric_limits::max()) { + ::close(fd); + set_err(err, "cannot map " + path + " (empty or too large)"); + return nullptr; + } + void* p = ::mmap(nullptr, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + ::close(fd); // the mapping stays valid + if (p == MAP_FAILED) { set_err(err, "cannot map " + path + " into memory"); return nullptr; } + std::unique_ptr m(new MappedFile()); + m->data_ = p; + m->size_ = (size_t)st.st_size; + return m; +} + +#endif + +} // namespace pk diff --git a/src/bundle_map.hpp b/src/bundle_map.hpp new file mode 100644 index 0000000..e5d3ef0 --- /dev/null +++ b/src/bundle_map.hpp @@ -0,0 +1,45 @@ +#pragma once +// A read-only memory map of a whole file, for the ced and voice components of a +// bundle (docs/bundle.md). ced.cpp and voice-detect.cpp load a bundle component +// from memory: they take the bytes of the whole bundle and a prefix, parse the +// header, and copy only the tensors of that component. Mapping the file means the +// operating system reads only the pages that are touched (the header and the +// component's tensors), so the other components are never read, and the map +// itself needs no private memory. +// +// The same code runs on Linux and macOS (mmap) and on Windows (MapViewOfFile). +// No temporary file is made. The file is opened read-only, mapped, and closed +// again before open() returns; the map stays valid until the object is destroyed. +// +// Keep the object alive only for the duration of the load. If another process +// shortens the file while it is mapped, a read past the new end can end the +// process (SIGBUS, or an access violation on Windows); model files must stay +// unchanged while they are loaded. +#include +#include +#include + +namespace pk { + +class MappedFile { +public: + // Returns nullptr and writes the reason to *err when the file cannot be + // opened, is empty, or is too large to map in this process. + static std::unique_ptr open(const std::string& path, std::string* err); + ~MappedFile(); + MappedFile(const MappedFile&) = delete; + MappedFile& operator=(const MappedFile&) = delete; + + const void* data() const { return data_; } + size_t size() const { return size_; } + +private: + MappedFile() = default; + const void* data_ = nullptr; + size_t size_ = 0; +#if defined(_WIN32) + void* mapping_ = nullptr; // HANDLE of the file mapping +#endif +}; + +} // namespace pk diff --git a/src/ced_tagger.cpp b/src/ced_tagger.cpp index b902e8f..00a3701 100644 --- a/src/ced_tagger.cpp +++ b/src/ced_tagger.cpp @@ -1,6 +1,7 @@ #include "ced_tagger.hpp" -#include "bundle_extract.hpp" +#include "bundle.hpp" +#include "bundle_map.hpp" #include "gguf.h" #ifdef PARAKEET_WITH_CED @@ -30,13 +31,27 @@ std::unique_ptr CedTagger::load(const std::string& path, const std::s if (component.empty()) { c = ced_capi_load(path.c_str()); } else { + // The bundle is mapped read-only and ced.cpp copies the tensors of the + // component out of the map during the call (docs/bundle.md). + BundleInfo info; std::string e; - std::unique_ptr cf = ComponentFile::create(path, component, &e); - if (!cf) { if (err) *err = e; return nullptr; } - c = ced_capi_load(cf->path().c_str()); // the file is removed when cf goes out of scope + if (!read_bundle_info(path, info, &e)) { if (err) *err = e; return nullptr; } + const BundleComponent* bc = info.find(component); + if (!bc) { if (err) *err = "the bundle has no component \"" + component + "\""; return nullptr; } + if (bc->kind != kBundleKindCed) { + if (err) *err = "component \"" + component + "\" is of kind \"" + bc->kind + "\", not \"ced\""; + return nullptr; + } + std::unique_ptr map = MappedFile::open(path, &e); + if (!map) { if (err) *err = e; return nullptr; } + c = ced_capi_load_from_memory_prefixed(map->data(), map->size(), (component + ".").c_str()); } if (!c) { - if (err && err->empty()) *err = "cannot load the sound model " + (component.empty() ? path : component); + if (err) { + const char* m = ced_capi_last_error(nullptr); + *err = "cannot load the sound model " + (component.empty() ? path : component) + + ((m && *m) ? std::string(": ") + m : std::string()); + } return nullptr; } std::unique_ptr t(new CedTagger()); diff --git a/src/ced_tagger.hpp b/src/ced_tagger.hpp index 9054232..89aaa78 100644 --- a/src/ced_tagger.hpp +++ b/src/ced_tagger.hpp @@ -19,9 +19,9 @@ class CedTagger { static bool available(); // nullptr on failure (or when unavailable). With a non-empty `component`, // `gguf_path` is a bundle GGUF (docs/bundle.md) and the model is its component - // of kind "ced". ced.cpp opens models by path only, so the component is first - // written as a standalone GGUF (an in-memory file on Linux, else a temporary - // file removed after the load; see bundle_extract.hpp). `err`, when given, + // of kind "ced". The bundle is mapped read-only and ced.cpp copies the + // component's tensors out of the map during the call: no temporary file, and + // the other components are not read (see bundle_map.hpp). `err`, when given, // receives the reason for a failure. static std::unique_ptr load(const std::string& gguf_path, const std::string& component = "", std::string* err = nullptr); diff --git a/src/speaker_encoder.cpp b/src/speaker_encoder.cpp index a7b0564..b6872b3 100644 --- a/src/speaker_encoder.cpp +++ b/src/speaker_encoder.cpp @@ -1,7 +1,7 @@ #include "speaker_encoder.hpp" #include "bundle.hpp" -#include "bundle_extract.hpp" +#include "bundle_map.hpp" #include "speaker_model_identity.hpp" #include "gguf.h" @@ -24,19 +24,19 @@ bool gguf_is_voicedetect(const std::string& path) { return vd; } -std::string speaker_encoder_family(const std::string& path, int dim_fallback) { +std::string speaker_encoder_family(const std::string& path, int dim_fallback, const std::string& prefix) { gguf_init_params p{/*no_alloc=*/true, /*ctx=*/nullptr}; gguf_context* g = gguf_init_from_file(path.c_str(), p); if (!g) return ""; - auto str = [&](const char* key) { - const int64_t id = gguf_find_key(g, key); + auto str = [&](const std::string& key) { + const int64_t id = gguf_find_key(g, (prefix + key).c_str()); return (id >= 0 && gguf_get_kv_type(g, id) == GGUF_TYPE_STRING) ? std::string(gguf_get_val_str(g, id)) : std::string(); }; std::string out; if (str("general.architecture") == "voicedetect") { long long dim = dim_fallback; - const int64_t id = gguf_find_key(g, "voicedetect.embedding_dim"); + const int64_t id = gguf_find_key(g, (prefix + "voicedetect.embedding_dim").c_str()); if (id >= 0) { const gguf_type t = gguf_get_kv_type(g, id); if (t == GGUF_TYPE_UINT32) dim = gguf_get_val_u32(g, id); @@ -61,48 +61,58 @@ bool SpeakerEncoder::available() { return true; } std::unique_ptr SpeakerEncoder::load(const std::string& path, const std::string& component, std::string* err) { std::string weights; - std::unique_ptr cf; - std::string load_path = path; try { if (component.empty()) { weights = speaker_model_identity(path); - } else { - BundleInfo info; - std::string e; - if (!read_bundle_info(path, info, &e)) { if (err) *err = e; return nullptr; } - const BundleComponent* c = info.find(component); - if (!c) { if (err) *err = "the bundle has no component \"" + component + "\""; return nullptr; } - const std::string& h = c->source_sha256; - bool hex = h.size() == 64; - for (char ch : h) hex = hex && ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f')); - if (!hex) { - if (err) *err = "voice component \"" + component + "\" has no valid source_sha256 in the bundle " - "header, which is its speaker model identity"; + voicedetect_ctx* c = voicedetect_capi_load(path.c_str()); + std::unique_ptr enc = adopt(c, path, "", weights, err); + if (enc && speaker_model_identity(path) != weights) { + if (err) *err = "the speaker model changed during load"; return nullptr; } - weights = "sha256:" + h; - cf = ComponentFile::create(path, component, &e); - if (!cf) { if (err) *err = e; return nullptr; } - load_path = cf->path(); // the file is removed when cf goes out of scope + return enc; } - std::unique_ptr enc = load_unchecked(load_path, weights, err); - if (enc && component.empty() && speaker_model_identity(path) != weights) { - if (err) *err = "the speaker model changed during load"; + BundleInfo info; + std::string e; + if (!read_bundle_info(path, info, &e)) { if (err) *err = e; return nullptr; } + const BundleComponent* c = info.find(component); + if (!c) { if (err) *err = "the bundle has no component \"" + component + "\""; return nullptr; } + if (c->kind != kBundleKindVoice) { + if (err) *err = "component \"" + component + "\" is of kind \"" + c->kind + "\", not \"voice\""; return nullptr; } - return enc; + const std::string& h = c->source_sha256; + bool hex = h.size() == 64; + for (char ch : h) hex = hex && ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f')); + if (!hex) { + if (err) *err = "voice component \"" + component + "\" has no valid source_sha256 in the bundle " + "header, which is its speaker model identity"; + return nullptr; + } + weights = "sha256:" + h; + // The bundle is mapped read-only and voice-detect.cpp copies the tensors of the + // component out of the map during the call (docs/bundle.md). + std::unique_ptr map = MappedFile::open(path, &e); + if (!map) { if (err) *err = e; return nullptr; } + const std::string prefix = component + "."; + voicedetect_ctx* vc = voicedetect_capi_load_from_memory_prefixed(map->data(), map->size(), prefix.c_str()); + return adopt(vc, path, prefix, weights, err); } catch (...) { if (err && err->empty()) *err = "cannot read the speaker model " + (component.empty() ? path : component); return nullptr; } } -std::unique_ptr SpeakerEncoder::load_unchecked(const std::string& path, - const std::string& weights, - std::string* err) { - voicedetect_ctx* c = voicedetect_capi_load(path.c_str()); +// Takes ownership of `c`, which came from loading `path` (for a bundle component, its +// keys carry `prefix`). +std::unique_ptr SpeakerEncoder::adopt(void* ctx, const std::string& path, const std::string& prefix, + const std::string& weights, std::string* err) { + voicedetect_ctx* c = static_cast(ctx); if (!c) { - if (err && err->empty()) *err = "cannot load the speaker model"; + if (err) { + const char* m = voicedetect_capi_last_load_error(); + *err = std::string("cannot load the speaker model") + ((m && *m) ? std::string(": ") + m : std::string()); + } return nullptr; } const int dim = voicedetect_capi_embedding_dim(c); @@ -114,7 +124,7 @@ std::unique_ptr SpeakerEncoder::load_unchecked(const std::string std::unique_ptr e(new SpeakerEncoder()); e->ctx_ = c; e->dim_ = dim; - e->fp_.family = speaker_encoder_family(path, dim); + e->fp_.family = speaker_encoder_family(path, dim, prefix); e->fp_.weights = weights; return e; } diff --git a/src/speaker_encoder.hpp b/src/speaker_encoder.hpp index 3a9b657..2c0fc73 100644 --- a/src/speaker_encoder.hpp +++ b/src/speaker_encoder.hpp @@ -18,9 +18,9 @@ class SpeakerEncoder { // nullptr on failure, when unavailable, or when the GGUF has no speaker // embedding (for example an age/gender/emotion model). With a non-empty // `component`, `gguf_path` is a bundle GGUF (docs/bundle.md) and the encoder is - // its component of kind "voice". voice-detect.cpp opens models by path only, so - // the component is first written as a standalone GGUF (an in-memory file on - // Linux, else a temporary file removed after the load; see bundle_extract.hpp). + // its component of kind "voice". The bundle is mapped read-only and + // voice-detect.cpp copies the component's tensors out of the map during the call: + // no temporary file, and the other components are not read (see bundle_map.hpp). // `err`, when given, receives the reason for a failure. static std::unique_ptr load(const std::string& gguf_path, const std::string& component = "", std::string* err = nullptr); @@ -42,8 +42,8 @@ class SpeakerEncoder { private: SpeakerEncoder() = default; - static std::unique_ptr load_unchecked(const std::string& path, - const std::string& weights, std::string* err); + static std::unique_ptr adopt(void* ctx, const std::string& path, const std::string& prefix, + const std::string& weights, std::string* err); void* ctx_ = nullptr; // voicedetect_ctx* int dim_ = 0; EncoderFingerprint fp_; @@ -54,8 +54,10 @@ class SpeakerEncoder { // "voicedetect:::" // A missing key leaves its field empty ("voicedetect::name:256"). "" when the file // is not a readable voice-detect GGUF. `dim_fallback` is used when the GGUF has no -// embedding_dim key (0 leaves the field empty). -std::string speaker_encoder_family(const std::string& gguf_path, int dim_fallback = 0); +// embedding_dim key (0 leaves the field empty). With a non-empty `prefix` (for a bundle +// component: ".") every key is looked up under that prefix. +std::string speaker_encoder_family(const std::string& gguf_path, int dim_fallback = 0, + const std::string& prefix = ""); // True when the GGUF's general.architecture is "voicedetect". Reads only the header. bool gguf_is_voicedetect(const std::string& gguf_path); diff --git a/tests/test_bundle.cpp b/tests/test_bundle.cpp index eafb6f2..386489f 100644 --- a/tests/test_bundle.cpp +++ b/tests/test_bundle.cpp @@ -17,7 +17,9 @@ #include #include "bundle.hpp" -#include "bundle_extract.hpp" +#include "bundle_map.hpp" +#include "ced_tagger.hpp" +#include "speaker_encoder.hpp" #include "ggml.h" #include "gguf.h" #include "model.hpp" @@ -384,54 +386,26 @@ int main() { CHECK(!pk::read_bundle_info(Bbad, info, &err)); } - // --- ComponentFile: a component as a standalone GGUF (for path-only third-party loaders) --- + // --- MappedFile: the read-only map handed to the ced and voice loaders --- { - char tmpl[] = "test_bundle_tmpdir_XXXXXX"; - const std::string tmpdir = ::mkdtemp(tmpl) ? tmpl : ""; - CHECK(!tmpdir.empty()); - for (int pass = 0; pass < 2; ++pass) { - if (pass == 1) { ::setenv("PARAKEET_BUNDLE_NO_MEMFD", "1", 1); ::setenv("TMPDIR", tmpdir.c_str(), 1); } - for (const char* comp : {"asr", "vad"}) { - const std::string& plain = std::string(comp) == "asr" ? plain_asr : plain_sil; - std::string cerr; - std::string path; - { - std::unique_ptr cf = pk::ComponentFile::create(B, comp, &cerr); - CHECK(cf != nullptr); - if (!cf) continue; - CHECK(cf->in_memory() == (pass == 0)); - path = cf->path(); - // Same keys, same types and the same tensor bytes as the single-model file. - ggml_context *c1 = nullptr, *c2 = nullptr; - gguf_context* g1 = gguf_init_from_file(plain.c_str(), {false, &c1}); - gguf_context* g2 = gguf_init_from_file(path.c_str(), {false, &c2}); - CHECK(g1 && g2); - if (g1 && g2) { - CHECK(gguf_get_n_kv(g1) == gguf_get_n_kv(g2) && gguf_get_n_tensors(g1) == gguf_get_n_tensors(g2)); - for (int64_t i = 0; i < gguf_get_n_kv(g1); ++i) { - const int64_t j = gguf_find_key(g2, gguf_get_key(g1, i)); - CHECK(j >= 0 && gguf_get_kv_type(g1, i) == gguf_get_kv_type(g2, j)); - } - for (ggml_tensor* t = ggml_get_first_tensor(c1); t; t = ggml_get_next_tensor(c1, t)) { - ggml_tensor* u = ggml_get_tensor(c2, t->name); - CHECK(u && u->type == t->type && ggml_nbytes(u) == ggml_nbytes(t) && - std::memcmp(u->data, t->data, ggml_nbytes(t)) == 0); - } - } - if (g1) gguf_free(g1); - if (g2) gguf_free(g2); - if (c1) ggml_free(c1); - if (c2) ggml_free(c2); - } - if (pass == 1) CHECK(access(path.c_str(), F_OK) != 0); // the temporary file is removed - } - std::string cerr; - CHECK(pk::ComponentFile::create(B, "nope", &cerr) == nullptr && !cerr.empty()); - CHECK(pk::ComponentFile::create("test_bundle.does-not-exist", "asr", &cerr) == nullptr); + std::ifstream in(B, std::ios::binary); + const std::string bytes((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + std::string merr; + { + std::unique_ptr m = pk::MappedFile::open(B, &merr); + CHECK(m != nullptr); + if (m) CHECK(m->size() == bytes.size() && std::memcmp(m->data(), bytes.data(), bytes.size()) == 0); } - ::unsetenv("PARAKEET_BUNDLE_NO_MEMFD"); - ::rmdir(tmpdir.c_str()); // fails if a temporary file was left behind - CHECK(access(tmpdir.c_str(), F_OK) != 0); + merr.clear(); + CHECK(pk::MappedFile::open("test_bundle.does-not-exist", &merr) == nullptr && !merr.empty()); + const std::string empty = dir + "empty.bin"; + { std::ofstream out(empty, std::ios::binary); } + merr.clear(); + CHECK(pk::MappedFile::open(empty, &merr) == nullptr && !merr.empty()); + // The loaders refuse a missing component and a component of another kind before touching any tensor. + CHECK(!pk::CedTagger::load(B, "nope", &merr)); + if (pk::CedTagger::available()) CHECK(merr.find("no component") != std::string::npos); + CHECK(!pk::SpeakerEncoder::load(B, "asr", &merr)); } // --- a VAD-only slice as a "vad" component (interplay with the standalone slice file) --- diff --git a/tests/test_bundle_full.cpp b/tests/test_bundle_full.cpp index ed4b05e..dcbde07 100644 --- a/tests/test_bundle_full.cpp +++ b/tests/test_bundle_full.cpp @@ -2,8 +2,8 @@ // built from: every component gives the same output as its standalone file. // ASR transcript, diarization JSON, CED class scores (bitwise), speaker embedding // (bitwise) and identity, Silero probabilities (bitwise), named diarization with -// bundle components. Also: partial loading, selection rules, and the temporary-file -// fallback of the ced and voice loaders (no file is left behind). +// bundle components. Also: partial loading, selection rules, and that loading a ced or +// voice component makes no temporary file, memory file or descriptor. // // LABEL model; run from the project root (fixtures are relative). // Env (skip 77 unless all are set): @@ -16,12 +16,12 @@ #include #include +#include #include #include #include "audio_io.hpp" #include "bundle.hpp" -#include "bundle_extract.hpp" #include "ced_tagger.hpp" #include "model.hpp" #include "parakeet_capi.h" @@ -41,6 +41,29 @@ static unsigned long long bytes_read() { std::fclose(f); return r; } +static unsigned long long storage_read_bytes() { // bytes this process made the kernel read from storage, mapped files included + std::FILE* f = std::fopen("/proc/self/io", "r"); + if (!f) return 0; + char k[64]; + unsigned long long v = 0, r = 0; + while (std::fscanf(f, "%63s %llu", k, &v) == 2) + if (std::string(k) == "read_bytes:") r = v; + std::fclose(f); + return r; +} +static void drop_page_cache(const char* path) { // clean pages of one file only; no privilege needed + const int fd = ::open(path, O_RDONLY); + if (fd >= 0) { ::posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED); ::close(fd); } +} +static bool maps_contain(const std::string& needle) { // any mapping or file name in /proc/self/maps + std::FILE* f = std::fopen("/proc/self/maps", "r"); + if (!f) return false; + char line[4096]; + bool hit = false; + while (!hit && std::fgets(line, sizeof line, f)) hit = std::strstr(line, needle.c_str()) != nullptr; + std::fclose(f); + return hit; +} static std::string take(char* p) { std::string s = p ? p : ""; if (p) parakeet_capi_free_string(p); @@ -156,23 +179,15 @@ int main() { CHECK(!solo.named_json.empty() && solo.id.rfind("sha256:", 0) == 0); std::printf("transcript: %s\ndiarization: %s\n", solo.transcript.c_str(), solo.diar_json.c_str()); - // ---- the same through the bundle, with the in-memory file and with the temporary-file fallback ---- + // ---- the same through the bundle. The temporary directory is read-only, so a load that tried + // to make a temporary file would fail; no descriptor, memory file or leftover may remain. ---- char tmpl[] = "/tmp/pk_bundle_full_XXXXXX"; const std::string tmpdir = ::mkdtemp(tmpl) ? tmpl : ""; CHECK(!tmpdir.empty()); - for (int pass = 0; pass < 2; ++pass) { - if (pass == 1) { - ::setenv("PARAKEET_BUNDLE_NO_MEMFD", "1", 1); - ::setenv("TMPDIR", tmpdir.c_str(), 1); - } - std::printf("-- pass %d (%s)\n", pass, pass ? "temporary file" : "memfd"); - { - auto cf = pk::ComponentFile::create(bundle, "ced", &err); - CHECK(cf && cf->in_memory() == (pass == 0)); - if (cf && pass == 1) CHECK(count_entries(tmpdir) == 1); - } - CHECK(count_entries(tmpdir) == 0); // the temporary file is gone once the object is - + ::chmod(tmpdir.c_str(), 0500); + ::setenv("TMPDIR", tmpdir.c_str(), 1); + const int cwd0 = count_entries("."); // nothing is created in the working directory either + { auto m = pk::Model::load(bundle, "asr"); CHECK(m != nullptr); if (m) CHECK(m->transcribe_path(speech) == solo.transcript); @@ -181,31 +196,47 @@ int main() { CHECK(d != nullptr); if (d) CHECK(take(parakeet_capi_diarize_path(d, two.c_str())) == solo.diar_json); - const unsigned long long r0 = bytes_read(); + const int fd0 = count_entries("/proc/self/fd"); + drop_page_cache(bundle); + const unsigned long long r0 = bytes_read(), f0 = storage_read_bytes(); std::string le; auto t = pk::CedTagger::load(bundle, "ced", &le); - const unsigned long long r1 = bytes_read(); + const unsigned long long r1 = bytes_read(), f1 = storage_read_bytes(); CHECK(t != nullptr); + CHECK(count_entries("/proc/self/fd") == fd0); // the loaded tagger holds no descriptor const pk::BundleComponent* cc = info.find("ced"); if (t) { std::vector p; pk::SoundScorer sc = t->scorer(); CHECK(sc(a_speech.samples.data(), (int)a_speech.samples.size(), p) && same_bits(p, solo.ced)); - if (r1 > r0 && cc) { - std::printf("ced: read %.1f MB (component %.1f MB, bundle %.1f MB)\n", (double)(r1 - r0) / 1e6, - (double)cc->n_bytes / 1e6, (double)st.st_size / 1e6); - CHECK(r1 - r0 < 3 * cc->n_bytes + (8u << 20)); // copy into the file, then the loader reads it - CHECK(r1 - r0 < (unsigned long long)st.st_size / 2); + // The bundle is mapped, not read(): only read() calls count in rchar (the header), and + // the pages touched through the map count as storage reads once the page cache of the + // file is dropped. Loading must read about the component (plus read-ahead), never the + // other components, which are over half of the bundle. The storage figure is 0 on a + // file system without block I/O accounting; the check is skipped then. + if (cc) { + const unsigned long long f = f1 - f0; + std::printf("ced: %.1f MB read(), %.1f MB read from storage (component %.1f MB, bundle %.1f MB)\n", + (double)(r1 - r0) / 1e6, (double)f / 1e6, (double)cc->n_bytes / 1e6, (double)st.st_size / 1e6); + CHECK(r1 - r0 < (8u << 20)); + CHECK(f < 2 * cc->n_bytes + (8u << 20)); + CHECK(f < (unsigned long long)st.st_size / 2); } } - const unsigned long long q0 = bytes_read(); + const unsigned long long q0 = bytes_read(), g0 = storage_read_bytes(); auto e = pk::SpeakerEncoder::load(bundle, "voice", &le); - const unsigned long long q1 = bytes_read(); + const unsigned long long q1 = bytes_read(), g1 = storage_read_bytes(); CHECK(e != nullptr); + CHECK(count_entries("/proc/self/fd") == fd0); // nor does the encoder if (e) { std::vector v; CHECK(e->embed(a_speech.samples.data(), (int)a_speech.samples.size(), v) && same_bits(v, solo.emb)); - if (q1 > q0 && vc) CHECK(q1 - q0 < 3 * vc->n_bytes + (8u << 20)); + if (vc) { + std::printf("voice: %.1f MB read(), %.1f MB read from storage (component %.1f MB)\n", (double)(q1 - q0) / 1e6, + (double)(g1 - g0) / 1e6, (double)vc->n_bytes / 1e6); + CHECK(q1 - q0 < (8u << 20)); + CHECK(g1 - g0 < 2 * vc->n_bytes + (8u << 20)); + } } parakeet_ctx* sp = parakeet_capi_load_component(bundle, "voice"); CHECK(sp != nullptr); @@ -224,9 +255,12 @@ int main() { parakeet_ctx* tg = parakeet_capi_load_component(bundle, "ced"); CHECK(tg && parakeet_capi_num_classes(tg) > 0 && parakeet_capi_class_label(tg, 0)); parakeet_capi_free(tg); parakeet_capi_free(d); parakeet_capi_free(sp); + // No temporary file, memory file or descriptor is left by any of the loads above. CHECK(count_entries(tmpdir) == 0); + CHECK(count_entries(".") == cwd0); + CHECK(!maps_contain("memfd:") && !maps_contain("parakeet-component") && !maps_contain(tmpdir)); } - ::unsetenv("PARAKEET_BUNDLE_NO_MEMFD"); + ::chmod(tmpdir.c_str(), 0700); ::rmdir(tmpdir.c_str()); if (failures) return 1; diff --git a/third_party/ced.cpp b/third_party/ced.cpp index 61dec2a..736a4ee 160000 --- a/third_party/ced.cpp +++ b/third_party/ced.cpp @@ -1 +1 @@ -Subproject commit 61dec2ab0106f2047ee40062a7075dbf08c523d0 +Subproject commit 736a4ee46a31d4ff38b41add65ce0f2cf1aaa05f diff --git a/third_party/voice-detect.cpp b/third_party/voice-detect.cpp index b74a896..bca46bc 160000 --- a/third_party/voice-detect.cpp +++ b/third_party/voice-detect.cpp @@ -1 +1 @@ -Subproject commit b74a896f47c6d04fcca0a962ff317528fd0b0019 +Subproject commit bca46bcbc2fe68169c2d7c414e9b290a7cb89911