Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ add_library(engine_core OBJECT
src/framework/debug/trace.cpp
src/framework/debug/profiler.cpp
src/framework/rknn/engine.cpp
src/framework/rknn/engine_resources.cpp
src/framework/decoders/moss_tts_delay/backbone.cpp
src/framework/decoders/moss_tts_delay/config.cpp
src/framework/decoders/moss_tts_delay/delay_decoder.cpp
Expand Down
11 changes: 6 additions & 5 deletions app/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,11 +412,12 @@ The next request reloads the model.

The optional free-memory guard checks host memory and GPU memory where available.
Its load estimate covers the selected weight file/package plus session auxiliary
files, with a runtime overhead factor and fixed floor. An `exec_plan` session
option is JSON rather than a path, so its declared artifacts are read out of the
plan and counted as files, each path resolved like an auxiliary file (relative to
the model directory) and counted once even when the plan declares a file the tree
scan already summed. A directory normally
files, with a runtime overhead factor and fixed floor. A family that holds
compiled engines resident reports its selected resource paths and final residency
through its loader. The family resolves its execution plan and applies its own
eviction policy; the guard only adds the reported bytes and excludes those files
from the weight scan. RKNN engines use driver-reported residency where available,
otherwise file size, without the weight runtime factor. A directory normally
selects `model.gguf` or its sole GGUF; several GGUFs without `model.gguf` are
ambiguous to the estimator. Unsupported family-specific layouts may skip the
guard with a log message, while an invalid spec-driven package can still fail
Expand Down
67 changes: 12 additions & 55 deletions app/server/model_memory.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#include "model_memory.h"

#include "engine/framework/assets/tensor_source.h"
#include "engine/framework/io/json.h"
#include "engine/framework/runtime/registry.h"

#include <filesystem>
#include <functional>
Expand Down Expand Up @@ -54,6 +54,12 @@ std::optional<size_t> estimate_model_memory_bytes(const ServerModelConfig & mode
}
}
};
// The model reports final residency; its resource files must not be counted again as weights.
const auto resources = engine::runtime::make_default_registry().model_resources(
model.family, model.path, model.session_options);
for (const auto & requirement : resources.engines) {
counted.insert(std::filesystem::weakly_canonical(requirement.path));
}
// Estimate only what the loader will actually read from model.path, so the
// guard neither overestimates nor masks the loader's own error:
// - a single-file model contributes that file;
Expand Down Expand Up @@ -94,59 +100,9 @@ std::optional<size_t> estimate_model_memory_bytes(const ServerModelConfig & mode
// is a directory, and against the model file's parent when it is a file.
const std::filesystem::path aux_base =
std::filesystem::is_directory(model.path, ec) ? model.path : model.path.parent_path();
for (const auto & [key, value] : model.session_options) {
// A compiled engine declared through an exec_plan is resident weight memory the loader
// reads, and the option's value is JSON, so the path scan below sees nothing: walk the
// artifact strings instead. A plan is declared per component role, and CosyVoice3's flow
// ladder packs several engines into one comma-separated value.
if (key.size() >= 10 && key.compare(key.size() - 10, 10, ".exec_plan") == 0) {
const auto plan = [](const std::string & option_value) -> engine::io::json::Value {
try {
return engine::io::json::parse(option_value);
} catch (const std::exception &) {
// The session reports a malformed plan itself; the guard stays silent.
return {};
}
}(value);
if (!plan.is_object()) {
continue;
}
for (const auto & [_, component] : plan.as_object()) {
if (!component.is_object()) {
continue;
}
const auto & members = component.as_object();
const auto artifacts = members.find("artifacts");
if (artifacts == members.end() || !artifacts->second.is_object()) {
continue;
}
for (const auto & [_, artifact] : artifacts->second.as_object()) {
if (!artifact.is_string()) {
continue;
}
const std::string & declared = artifact.as_string();
size_t start = 0;
while (start <= declared.size()) {
const auto end = declared.find(',', start);
const auto stop = end == std::string::npos ? declared.size() : end;
if (stop > start) {
std::filesystem::path engine_path =
std::filesystem::path(declared.substr(start, stop - start));
if (engine_path.is_relative()) {
engine_path = aux_base / engine_path;
}
add_file(engine_path);
}
if (end == std::string::npos) {
break;
}
start = end + 1;
}
}
}
continue;
}
std::filesystem::path aux(value);
for (const auto & option : model.session_options) {
// Only file-valued options contribute; opaque values are not interpreted here.
std::filesystem::path aux(option.second);
if (aux.is_relative()) {
aux = aux_base / aux;
}
Expand All @@ -156,7 +112,8 @@ std::optional<size_t> estimate_model_memory_bytes(const ServerModelConfig & mode
// KV state, plus a fixed floor for per-model bookkeeping.
constexpr double kRuntimeOverheadFactor = 1.5;
constexpr size_t kFixedOverhead = 128ull * 1024 * 1024;
return static_cast<size_t>(static_cast<double>(weights) * kRuntimeOverheadFactor) + kFixedOverhead;
return static_cast<size_t>(static_cast<double>(weights) * kRuntimeOverheadFactor) + kFixedOverhead +
resources.resident_bytes;
}

} // namespace minitts::server
6 changes: 5 additions & 1 deletion app/server/model_memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
namespace minitts::server {

// Estimated resident bytes a model will occupy once loaded (weights plus a
// runtime overhead factor for GPU buffers / compute graphs). Returns nullopt
// runtime overhead factor for GPU buffers / compute graphs). A family that
// declares auxiliary resources reports their paths and final resident bytes
// through its loader. The server excludes those paths from weight scans and adds
// the reported bytes without interpreting execution plans or residency policies.
// Returns nullopt
// when the footprint is indeterminate: a model directory holding several GGUFs
// and no model.gguf. The guard must not refuse such a load with a 503 -- the
// loader either rejects the ambiguous directory itself (the spec-driven path
Expand Down
18 changes: 10 additions & 8 deletions include/engine/framework/rknn/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ class ApertureBudget {
// returned unchanged, so a failed load quotes exactly what was asked for.
std::string resolve_engine_path(const std::string & artifact, const std::filesystem::path & model_root);

// What a compiled engine will occupy once it is resident, in MiB, read from the driver itself:
// `rknn_init` with `RKNN_FLAG_COLLECT_MODEL_INFO_ONLY` parses the image without building the network
// (~0.5 s per engine on RK3588, no NPU memory allocated) and `rknn_query(RKNN_QUERY_MEM_SIZE)` then
// reports `total_weight_size + total_internal_size` -- the bytes the real init goes on to allocate,
// less a per-context slack of 30-50 MiB. Returns 0 when the driver cannot say, so a caller can keep
// a proxy for that case; a build without an RKNN runtime is that case for every engine, so the
// declaration is not conditional and a consumer (a sidecar, or the server's load guard) needs no
// preprocessor test of its own to ask.
int64_t engine_memory_mib(const std::string & path, const std::string & label);

#if defined(ENGINE_HAVE_RKNN)

// One graph input for `Engine::run`. `size` is the size of the buffer in bytes: with
Expand All @@ -82,14 +92,6 @@ struct Input {
uint32_t pass_through = 0;
};

// What a compiled engine will occupy once it is resident, in MiB, read from the driver itself:
// `rknn_init` with `RKNN_FLAG_COLLECT_MODEL_INFO_ONLY` parses the image without building the network
// (~0.5 s per engine on RK3588, no NPU memory allocated) and `rknn_query(RKNN_QUERY_MEM_SIZE)` then
// reports `total_weight_size + total_internal_size` -- the bytes the real init goes on to allocate,
// less a per-context slack of 30-50 MiB. Returns 0 when the driver cannot say, so a sidecar can keep
// a proxy for that case.
int64_t engine_memory_mib(const std::string & path, const std::string & label);

// One compiled engine: a `rknn_context` plus its input/output counts.
class Engine {
public:
Expand Down
18 changes: 18 additions & 0 deletions include/engine/framework/rknn/engine_resources.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

#include "engine/framework/runtime/model.h"

#include <filesystem>
#include <string>
#include <vector>

namespace engine::rknn {

// Selected engine artifacts may each contain a comma-separated list of paths. Resolve and
// deduplicate them, price through the driver (file size when unavailable), and sum residency.
// Component/artifact selection and eviction policy belong to the caller.
runtime::ModelResourcePlan engine_resources(
const std::vector<std::string> & artifacts,
const std::filesystem::path & model_path);

} // namespace engine::rknn
31 changes: 31 additions & 0 deletions include/engine/framework/runtime/model.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "engine/framework/runtime/session.h"

#include <cstddef>
#include <filesystem>
#include <optional>
#include <memory>
Expand Down Expand Up @@ -116,6 +117,23 @@ struct LoaderAdvertisement {
std::vector<std::string> api_endpoints;
};

/**
* One selected resource, with its model-reported resident cost in bytes.
*/
struct ModelEngineRequirement {
std::filesystem::path path;
size_t resident_bytes = 0;
};

/**
* Resources beyond weight files and their aggregate residency. The model applies its own
* residency policy; consumers use the paths for deduplication and add resident_bytes as given.
*/
struct ModelResourcePlan {
std::vector<ModelEngineRequirement> engines;
size_t resident_bytes = 0;
};

/** Map advertised capabilities to the HTTP surfaces they normally use. */
std::vector<std::string> default_api_endpoints_for_capabilities(const CapabilitySet & capabilities);

Expand All @@ -138,6 +156,19 @@ class IVoiceModelLoader {
virtual std::string advertised_instructions_policy() const;
virtual std::vector<std::string> advertised_api_endpoints() const;
LoaderAdvertisement advertise() const;

/**
* The artifacts this family will hold resident for one load of `model_path` under these
* session options, priced by the family. A family whose stages are all ggml graphs reports
* nothing: its host-consumer estimate is the weight files plus the caller's runtime factor.
*/
virtual ModelResourcePlan model_resources(
const std::filesystem::path & model_path,
const std::unordered_map<std::string, std::string> & session_options) const {
(void)model_path;
(void)session_options;
return {};
}
};

} // namespace engine::runtime
10 changes: 10 additions & 0 deletions include/engine/framework/runtime/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ class ModelRegistry {
std::unique_ptr<ILoadedVoiceModel> load(const ModelLoadRequest & request) const;
std::unique_ptr<ILoadedVoiceModel> load(const std::filesystem::path & model_path) const;

/**
* What the named family's session options ask for beyond weight files, from the loader that
* owns the family (see IVoiceModelLoader::model_resources). A family with no loader registered
* in this registry -- or one that reports nothing -- yields an empty plan.
*/
ModelResourcePlan model_resources(
const std::string & family,
const std::filesystem::path & model_path,
const std::unordered_map<std::string, std::string> & session_options) const;

private:
void validate_request(const ModelLoadRequest & request) const;
const IVoiceModelLoader * find_loader(const ModelLoadRequest & request) const;
Expand Down
11 changes: 11 additions & 0 deletions include/engine/framework/runtime/spec_backed_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ struct SpecBackedVoiceModelConfig {
const SessionOptions &,
std::shared_ptr<const Assets>,
std::shared_ptr<const engine::model_spec::ModelContract>)> create_session;
std::function<ModelResourcePlan(
const std::filesystem::path &,
const std::unordered_map<std::string, std::string> &)> model_resources;
};

inline std::shared_ptr<const engine::model_spec::ModelContract> require_model_contract(std::string family) {
Expand Down Expand Up @@ -243,6 +246,14 @@ class SpecBackedVoiceModelLoader final : public IVoiceModelLoader {
std::move(assets));
}

ModelResourcePlan model_resources(
const std::filesystem::path & model_path,
const std::unordered_map<std::string, std::string> & session_options) const override {
return config_.model_resources
? config_.model_resources(model_path, session_options)
: ModelResourcePlan{};
}

private:
SpecBackedVoiceModelConfig<Assets> config_;
};
Expand Down
6 changes: 6 additions & 0 deletions include/engine/models/cosyvoice3/rknn_flow.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,10 @@ class CosyVoice3RknnFlow {
std::unique_ptr<Impl> impl_;
};

// The resident-set ceiling this sidecar enforces on its ladder, in MiB: the value
// `AUDIOCPP_RKNN_APERTURE_MIB` sets, else this board's measured ceiling, and 0 when the accounting
// is off and every declared rung is pinned. A host consumer (the server's load guard) asks for it
// through the loader's resource plan, which is where a family's residency policy belongs.
int64_t aperture_budget_mib();

} // namespace engine::models::cosyvoice3
23 changes: 23 additions & 0 deletions src/community_models/inflect_v2/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

#include "engine/community_models/inflect_v2/rknn_decoder.h"
#include "engine/framework/debug/profiler.h"
#include "engine/framework/io/json.h"
#include "engine/framework/rknn/engine.h"
#include "engine/framework/rknn/engine_resources.h"
#include "engine/framework/runtime/options.h"
#include "engine/framework/runtime/spec_backed_model.h"
#include "engine/framework/text/utf8.h"
Expand Down Expand Up @@ -223,6 +225,27 @@ std::shared_ptr<runtime::IVoiceModelLoader> make_inflect_v2_loader() {
runtime::SpecBackedVoiceModelConfig<InflectV2Assets> config;
config.family = kFamily;
config.load_assets = load_inflect_v2_assets;
config.model_resources = [](const std::filesystem::path & path,
const std::unordered_map<std::string, std::string> & options) {
const auto option = options.find("inflect_v2.exec_plan");
if (option == options.end()) {
return runtime::ModelResourcePlan{};
}
std::vector<std::string> artifacts;
try {
const auto plan = io::json::parse(option->second);
const auto * decoder = plan.find("decoder");
const auto * impl = decoder ? decoder->find("impl") : nullptr;
if (!impl || impl->as_string() != "rknn") {
return runtime::ModelResourcePlan{};
}
artifacts.push_back(io::json::require_string(decoder->require("artifacts"), "main"));
} catch (const std::runtime_error &) {
// Session loading reports invalid execution plans.
return runtime::ModelResourcePlan{};
}
return rknn::engine_resources(artifacts, path);
};
config.create_session = [](
const runtime::TaskSpec & task,
const runtime::SessionOptions & options,
Expand Down
23 changes: 23 additions & 0 deletions src/community_models/piper_tts/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

#include "engine/framework/core/backend.h"
#include "engine/framework/debug/profiler.h"
#include "engine/framework/io/json.h"
#include "engine/framework/rknn/engine.h"
#include "engine/framework/rknn/engine_resources.h"
#include "engine/framework/runtime/options.h"
#include "engine/framework/runtime/spec_backed_model.h"
#include "engine/framework/text/chunking.h"
Expand Down Expand Up @@ -229,6 +231,27 @@ std::shared_ptr<runtime::IVoiceModelLoader> make_piper_tts_loader() {
runtime::SpecBackedVoiceModelConfig<PiperTtsAssets> config;
config.family = kFamily;
config.load_assets = load_piper_tts_assets;
config.model_resources = [](const std::filesystem::path & path,
const std::unordered_map<std::string, std::string> & options) {
const auto option = options.find("piper_tts.exec_plan");
if (option == options.end()) {
return runtime::ModelResourcePlan{};
}
std::vector<std::string> artifacts;
try {
const auto plan = io::json::parse(option->second);
const auto * decoder = plan.find("decoder");
const auto * impl = decoder ? decoder->find("impl") : nullptr;
if (!impl || impl->as_string() != "rknn") {
return runtime::ModelResourcePlan{};
}
artifacts.push_back(io::json::require_string(decoder->require("artifacts"), "main"));
} catch (const std::runtime_error &) {
// Session loading reports invalid execution plans.
return runtime::ModelResourcePlan{};
}
return rknn::engine_resources(artifacts, path);
};
config.create_session = [](
const runtime::TaskSpec & task,
const runtime::SessionOptions & options,
Expand Down
6 changes: 6 additions & 0 deletions src/framework/rknn/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ std::string driver_report() {
return {};
}

int64_t engine_memory_mib(const std::string & /* path */, const std::string & /* label */) {
// No driver to ask, so every engine reports "unknown" -- the answer a caller's proxy is written
// to fall back on.
return 0;
}

#endif // ENGINE_HAVE_RKNN

} // namespace engine::rknn
Loading
Loading