From 1e6f4d1284ede148e5c2442b2c649426023a4b99 Mon Sep 17 00:00:00 2001 From: zhenghaoz Date: Tue, 27 Oct 2026 00:40:04 +0800 Subject: [PATCH] [verified] refactor: separate model loading and execution contexts --- src/model-context.h | 33 ++++++++++++++++++ src/model.h | 5 +++ src/models/clamp3.cpp | 72 ++++++++++++++++++++++++++++++++++------ src/models/clap.cpp | 63 ++++++++++++++++++++++++++++++----- src/models/mert.cpp | 61 +++++++++++++++++++++++++++++----- src/models/mert2.cpp | 48 +++++++++++++++++++++++---- src/models/models.h | 67 +++++++++++++++---------------------- src/models/muq.cpp | 58 +++++++++++++++++++++++++++----- src/models/muq_mulan.cpp | 60 ++++++++++++++++++++++++++++----- 9 files changed, 375 insertions(+), 92 deletions(-) create mode 100644 src/model-context.h diff --git a/src/model-context.h b/src/model-context.h new file mode 100644 index 0000000..cd9b6ce --- /dev/null +++ b/src/model-context.h @@ -0,0 +1,33 @@ +#pragma once + +#include "model.h" +#include "model-ops.h" + +namespace clap::detail { + +// Shared execution state for architecture-specific encoders, corresponding to +// llama.cpp's common graph context. This runtime executes TensorOps eagerly: +// it is not a persistent GGML graph. Keep the context alive across calls so +// backend selection and uploaded-weight caches retain their existing lifetime. +// The model's validated parameters and loader must outlive this context. +class ModelContext { +public: + ModelContext(const ModelLoader &source, int threads) : loader(source), ops(source, threads) {} + virtual ~ModelContext() = default; + ModelContext(const ModelContext &) = delete; + ModelContext &operator=(const ModelContext &) = delete; + + virtual std::vector text(const std::vector &ids) const = 0; + virtual AudioFrames audio_frames(const std::vector &pcm) const = 0; + virtual AudioFrames clamp3_features(const std::vector &) const { + throw std::runtime_error("CLaMP3 feature extraction requires a MERT-v1 model"); + } + virtual std::vector waveform_embedding(const std::vector &, size_t = 0) const { + throw std::runtime_error("CLAP: this architecture has no waveform embeddings"); + } + +protected: + const ModelLoader &loader; + TensorOps ops; +}; +} // namespace clap::detail diff --git a/src/model.h b/src/model.h index 1df9ffb..28c1623 100644 --- a/src/model.h +++ b/src/model.h @@ -2,10 +2,12 @@ #include "clap.h" #include +#include #include #include namespace clap::detail { +class ModelContext; struct TextParameters { int bos; int eos; @@ -37,6 +39,9 @@ class ModelArchitecture { throw std::runtime_error("CLAP: this architecture has no waveform embeddings"); } protected: + // Architecture-local execution is constructed only after parameters and + // tensor contracts are validated, like llama.cpp build_arch_graph(). + virtual std::unique_ptr build_arch_context(int threads) const = 0; virtual void load_arch_hparams() = 0; virtual void load_arch_tensors() const = 0; }; diff --git a/src/models/clamp3.cpp b/src/models/clamp3.cpp index 77ecd52..90c60ea 100644 --- a/src/models/clamp3.cpp +++ b/src/models/clamp3.cpp @@ -1,4 +1,5 @@ #include "models.h" +#include "model-context.h" #include #include #include @@ -7,8 +8,44 @@ namespace clap::detail { using json = nlohmann::json; +// Execution is kept separate from the validated model/tensor contract below. +struct Clamp3Model::context final : ModelContext { + context(const Clamp3Model &model, int threads) + : ModelContext(model.loader, threads), + model(model), + config(model.config), + audio_config(model.audio_config), + text_config(model.text_config), + frontend(model.frontend) {} + + int projection_dimension() const { return model.projection_dimension(); } + std::vector encode_features(const std::vector &features, int frames) const; + Matrix encode(const Matrix &embeddings, const std::vector &mask, + const nlohmann::json &branch, const std::string &prefix) const; + Matrix attention(const Matrix &x, const std::vector &mask, + const std::string &prefix, int heads) const; + std::vector chunked_text(const std::vector &ids) const; + std::vector chunked_audio(const Matrix &features) const; + std::vector pool_project(const Matrix &hidden, const std::vector &mask, + const std::string &projection) const; + std::vector text(const std::vector &ids) const override; + AudioFrames audio_frames(const std::vector &pcm) const override; + std::vector waveform_embedding(const std::vector &pcm, size_t crop_offset = 0) const override; + +private: + const Clamp3Model &model; + const nlohmann::json &config; + const nlohmann::json &audio_config; + const nlohmann::json &text_config; + const std::unique_ptr &frontend; +}; + +std::unique_ptr Clamp3Model::build_arch_context(int threads) const { + return std::make_unique(*this, threads); +} + Clamp3Model::Clamp3Model(const ModelLoader &source, int threads) - : loader(source), ops(source, threads) { + : loader(source) { load_arch_hparams(); load_arch_tensors(); if (loader.has_metadata_prefix("clamp3.audio_frontend")) { @@ -25,10 +62,11 @@ Clamp3Model::Clamp3Model(const ModelLoader &source, int threads) } else { throw std::runtime_error("CLAP: CLaMP3 requires bundled MERT-v1-95M audio frontend metadata; reconvert the checkpoint"); } + inference = build_arch_context(threads); } Clamp3Model::~Clamp3Model() = default; bool Clamp3Model::has_waveform_embeddings() const { return frontend != nullptr; } -std::vector Clamp3Model::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { +std::vector Clamp3Model::context::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { if (crop_offset != 0) throw std::runtime_error("CLAP: crop offsets are unsupported for this model"); if (!frontend) throw std::runtime_error("CLAP: CLaMP3 requires MERT-v1-95M features"); const auto features = frontend->clamp3_features(pcm); @@ -117,11 +155,11 @@ int Clamp3Model::projection_dimension() const { return config.at("projection_dim int Clamp3Model::mel_bins() const { return 0; } int Clamp3Model::sample_rate() const { return frontend ? frontend->sample_rate() : 0; } bool Clamp3Model::is_waveform_encoder() const { return false; } -AudioFrames Clamp3Model::audio_frames(const std::vector &) const { +AudioFrames Clamp3Model::context::audio_frames(const std::vector &) const { throw std::runtime_error("CLAP: CLaMP3 requires precomputed MERT-v1-95M features"); } -Matrix Clamp3Model::attention(const Matrix &x, const std::vector &mask, +Matrix Clamp3Model::context::attention(const Matrix &x, const std::vector &mask, const std::string &prefix, int heads) const { const auto q = ops.linear(x, prefix + ".query"); const auto k = ops.linear(x, prefix + ".key"); @@ -151,7 +189,7 @@ Matrix Clamp3Model::attention(const Matrix &x, const std::vector &mask, return result; } -Matrix Clamp3Model::encode(const Matrix &embeddings, const std::vector &mask, +Matrix Clamp3Model::context::encode(const Matrix &embeddings, const std::vector &mask, const json &branch, const std::string &prefix) const { Matrix x = ops.norm(embeddings, prefix + ".embeddings.LayerNorm", branch.at("layer_norm_eps")); for (int layer = 0; layer < branch.at("num_hidden_layers").get(); ++layer) { @@ -169,7 +207,7 @@ Matrix Clamp3Model::encode(const Matrix &embeddings, const std::vector &mas return x; } -std::vector Clamp3Model::pool_project(const Matrix &hidden, const std::vector &mask, +std::vector Clamp3Model::context::pool_project(const Matrix &hidden, const std::vector &mask, const std::string &projection) const { int count = 0; Matrix pooled(1, hidden.cols); @@ -183,7 +221,7 @@ std::vector Clamp3Model::pool_project(const Matrix &hidden, const std::ve return ops.linear(pooled, projection).data; } -std::vector Clamp3Model::chunked_text(const std::vector &ids) const { +std::vector Clamp3Model::context::chunked_text(const std::vector &ids) const { const int chunk = config.at("chunk_length"), dim = text_config.at("hidden_size"); const int pad = text_config.at("pad_token_id"), vocab = text_config.at("vocab_size"); const auto *words = loader.weight("text.embeddings.word_embeddings.weight"); @@ -223,7 +261,7 @@ std::vector Clamp3Model::chunked_text(const std::vector &ids) const return result; } -std::vector Clamp3Model::chunked_audio(const Matrix &features) const { +std::vector Clamp3Model::context::chunked_audio(const Matrix &features) const { const int chunk = config.at("chunk_length"), dim = audio_config.at("hidden_size"); const auto *positions = loader.weight("audio.embeddings.position_embeddings.weight"); const auto *types = loader.weight("audio.embeddings.token_type_embeddings.weight"); @@ -257,13 +295,13 @@ std::vector Clamp3Model::chunked_audio(const Matrix &features) const { return result; } -std::vector Clamp3Model::text(const std::vector &ids) const { +std::vector Clamp3Model::context::text(const std::vector &ids) const { if (ids.empty()) throw std::runtime_error("CLAP: text token sequence is empty"); return chunked_text(ids); } -std::vector Clamp3Model::encode_features(const std::vector &features, int frames) const { +std::vector Clamp3Model::context::encode_features(const std::vector &features, int frames) const { const int dim = config.at("audio_feature_dim"); if (!(frames > 0 && frames <= std::numeric_limits::max() - 2 && features.size() == static_cast(frames) * dim)) @@ -274,4 +312,18 @@ std::vector Clamp3Model::encode_features(const std::vector &featur std::copy(features.begin(), features.end(), bordered.data.begin() + dim); return chunked_audio(bordered); } + +// Model-facing entry points preserve the existing internal/public facade. +std::vector Clamp3Model::encode_features(const std::vector &features, int frames) const { + return static_cast(*inference).encode_features(features, frames); +} +std::vector Clamp3Model::text(const std::vector &ids) const { + return inference->text(ids); +} +AudioFrames Clamp3Model::audio_frames(const std::vector &pcm) const { + return inference->audio_frames(pcm); +} +std::vector Clamp3Model::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { + return inference->waveform_embedding(pcm, crop_offset); +} } // namespace clap::detail diff --git a/src/models/clap.cpp b/src/models/clap.cpp index 69c1d58..fe4a599 100644 --- a/src/models/clap.cpp +++ b/src/models/clap.cpp @@ -1,4 +1,5 @@ #include "models.h" +#include "model-context.h" #include #include #include @@ -18,10 +19,41 @@ float cubic(float x) { } } // namespace -ClapModel::ClapModel(const ModelLoader &source, int threads) : loader(source), ops(source, threads) { +// Execution is kept separate from the validated model/tensor contract below. +struct ClapModel::context final : ModelContext { + context(const ClapModel &model, int threads) + : ModelContext(model.loader, threads), + model(model), + config(model.config), + a(model.a), + t(model.t) {} + + int mel_bins() const { return model.mel_bins(); } + std::vector encode_features(const std::vector &features, int frames) const; + Matrix attention(const Matrix &x, const std::string &prefix, int heads, int tokens, + const std::vector *mask = nullptr, int window = 0, int shift = 0, int grid = 0) const; + std::vector project(const Matrix &pooled, const std::string &prefix) const; + std::vector text(const std::vector &ids) const override; + AudioFrames audio_frames(const std::vector &pcm) const override; + std::vector waveform_embedding(const std::vector &pcm, size_t crop_offset = 0) const override; + +private: + const ClapModel &model; + const nlohmann::json &config; + const nlohmann::json &a; + const nlohmann::json &t; +}; + +std::unique_ptr ClapModel::build_arch_context(int threads) const { + return std::make_unique(*this, threads); +} + +ClapModel::ClapModel(const ModelLoader &source, int threads) : loader(source) { load_arch_hparams(); load_arch_tensors(); + inference = build_arch_context(threads); } +ClapModel::~ClapModel() = default; void ClapModel::load_arch_hparams() { config = json::parse(loader.metadata("clap.config")); @@ -117,11 +149,11 @@ int ClapModel::projection_dimension() const { return config.at("projection_dim") int ClapModel::mel_bins() const { return a.at("num_mel_bins"); } int ClapModel::sample_rate() const { return 48000; } bool ClapModel::is_waveform_encoder() const { return false; } -AudioFrames ClapModel::audio_frames(const std::vector &) const { +AudioFrames ClapModel::context::audio_frames(const std::vector &) const { throw std::runtime_error("CLAP: CLAP exposes one pooled audio projection, not frame representations"); } -Matrix ClapModel::attention(const Matrix &x, const std::string &prefix, int heads, int tokens, +Matrix ClapModel::context::attention(const Matrix &x, const std::string &prefix, int heads, int tokens, const std::vector *mask, int window, int shift, int grid) const { const auto q = ops.linear(x, prefix + ".query"), k = ops.linear(x, prefix + ".key"), v = ops.linear(x, prefix + ".value"); Matrix result(x.rows, x.cols); @@ -163,7 +195,7 @@ Matrix ClapModel::attention(const Matrix &x, const std::string &prefix, int head return result; } -std::vector ClapModel::project(const Matrix &pooled, const std::string &prefix) const { +std::vector ClapModel::context::project(const Matrix &pooled, const std::string &prefix) const { auto h = ops.linear(pooled, prefix + ".linear1"); for (float &v : h.data) v = std::max(v, 0.0f); auto y = ops.linear(h, prefix + ".linear2"); @@ -176,7 +208,7 @@ std::vector ClapModel::project(const Matrix &pooled, const std::string &p return y.data; } -std::vector ClapModel::text(const std::vector &ids) const { +std::vector ClapModel::context::text(const std::vector &ids) const { const int dim = t.at("hidden_size"), pad = t.at("pad_token_id"), vocab = t.at("vocab_size"); if (!(!ids.empty() && ids.size() <= static_cast(t.at("max_position_embeddings").get() - 2))) throw std::runtime_error("CLAP: text length outside model context"); @@ -217,12 +249,12 @@ std::vector ClapModel::text(const std::vector &ids) const { return project(pooled, "text_proj"); } -std::vector ClapModel::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { +std::vector ClapModel::context::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { if (mel_bins() != 64) throw std::runtime_error("CLAP: waveform frontend requires 64 mel bins"); return encode_features(clap::log_mel(pcm, 48000, crop_offset), 1001); } -std::vector ClapModel::encode_features(const std::vector &features, int frames) const { +std::vector ClapModel::context::encode_features(const std::vector &features, int frames) const { const int bins = a.at("num_mel_bins"), spec = a.at("spec_size"), ratio = spec / bins; const int target_time = spec * ratio; if (!(frames > 0 && frames <= target_time && features.size() == static_cast(frames) * bins)) @@ -327,4 +359,19 @@ std::vector ClapModel::encode_features(const std::vector &features Matrix pooled(1, x.cols); for (int r = 0; r < x.rows; ++r) for (int c = 0; c < x.cols; ++c) pooled[0][c] += x[r][c] / x.rows; return project(pooled, "audio_proj"); -}} // namespace clap::detail +} + +// Model-facing entry points preserve the existing internal/public facade. +std::vector ClapModel::encode_features(const std::vector &features, int frames) const { + return static_cast(*inference).encode_features(features, frames); +} +std::vector ClapModel::text(const std::vector &ids) const { + return inference->text(ids); +} +AudioFrames ClapModel::audio_frames(const std::vector &pcm) const { + return inference->audio_frames(pcm); +} +std::vector ClapModel::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { + return inference->waveform_embedding(pcm, crop_offset); +} +} // namespace clap::detail diff --git a/src/models/mert.cpp b/src/models/mert.cpp index 88a3317..84b8d08 100644 --- a/src/models/mert.cpp +++ b/src/models/mert.cpp @@ -1,4 +1,5 @@ #include "models.h" +#include "model-context.h" #include "ggml-cpu.h" #include #include @@ -6,12 +7,43 @@ #include namespace clap::detail { +// Execution is kept separate from the validated model/tensor contract below. +struct MertModel::context final : ModelContext { + context(const MertModel &model, int threads) + : ModelContext(model.loader, threads), + model(model), + config(model.config), + minimum_samples(model.minimum_samples) {} + + int projection_dimension() const { return model.projection_dimension(); } + std::vector normalize(const std::vector &pcm) const; + Matrix forward(const std::vector &normalized, std::vector *layer_mean) const; + Matrix convolution(const Matrix &x, const std::string &name, int kernel, + int stride, int padding, int groups, bool bias) const; + Matrix linear(const Matrix &x, const std::string &name) const; + Matrix attention(const Matrix &x, const std::string &name) const; + std::vector text(const std::vector &) const override; + AudioFrames audio_frames(const std::vector &pcm) const override; + AudioFrames clamp3_features(const std::vector &pcm) const override; + +private: + const MertModel &model; + const nlohmann::json &config; + const int minimum_samples; +}; + +std::unique_ptr MertModel::build_arch_context(int threads) const { + return std::make_unique(*this, threads); +} + MertModel::MertModel(const ModelLoader &source, int threads, const std::string &tensor_prefix, const std::string &metadata_prefix) - : loader(source, tensor_prefix, metadata_prefix), ops(loader, threads) { + : loader(source, tensor_prefix, metadata_prefix) { load_arch_hparams(); load_arch_tensors(); + inference = build_arch_context(threads); } +MertModel::~MertModel() = default; void MertModel::load_arch_hparams() { config = nlohmann::json::parse(loader.metadata("mert.config")); @@ -88,11 +120,11 @@ void MertModel::load_arch_tensors() const { } } -std::vector MertModel::text(const std::vector &) const { +std::vector MertModel::context::text(const std::vector &) const { throw std::runtime_error("MERT-v1 has no text encoder"); } -std::vector MertModel::normalize(const std::vector &pcm) const { +std::vector MertModel::context::normalize(const std::vector &pcm) const { if (pcm.size() < static_cast(minimum_samples) || pcm.size() > (1ULL << 28)) throw std::runtime_error("MERT waveform is empty, too short or too large"); double mean = 0, variance = 0; @@ -110,7 +142,7 @@ std::vector MertModel::normalize(const std::vector &pcm) const { // Grouped im2col + GGML matrix multiply, bounded to one group's workspace. // Stored convolution weights [out,in/group,kernel] are already dense in tap order. -Matrix MertModel::convolution(const Matrix &x, const std::string &name, int kernel, +Matrix MertModel::context::convolution(const Matrix &x, const std::string &name, int kernel, int stride, int padding, int groups, bool bias) const { const auto *weight = loader.weight(name + ".weight"); const int outputs = static_cast(weight->ne[2]); @@ -151,7 +183,7 @@ Matrix MertModel::convolution(const Matrix &x, const std::string &name, int kern // F16 is storage-only: GGML's F16 dot path also rounds activations, which // compounds through 12 encoder layers. Expand one dense weight at a time. -Matrix MertModel::linear(const Matrix &x, const std::string &name) const { +Matrix MertModel::context::linear(const Matrix &x, const std::string &name) const { const auto *weight = loader.weight(name + ".weight"); if (weight->type == GGML_TYPE_F32 || ggml_is_quantized(weight->type)) return ops.linear(x, name); if (weight->type != GGML_TYPE_F16) @@ -178,7 +210,7 @@ Matrix MertModel::linear(const Matrix &x, const std::string &name) const { return result; } -Matrix MertModel::attention(const Matrix &x, const std::string &name) const { +Matrix MertModel::context::attention(const Matrix &x, const std::string &name) const { auto q = linear(x,name+".q_proj"), k = linear(x,name+".k_proj"), v = linear(x,name+".v_proj"); Matrix out(x.rows,x.cols); const int heads = config.at("num_attention_heads"), width = x.cols / heads; @@ -199,7 +231,7 @@ Matrix MertModel::attention(const Matrix &x, const std::string &name) const { return linear(out,name+".out_proj"); } -Matrix MertModel::forward(const std::vector &pcm, std::vector *layer_mean) const { +Matrix MertModel::context::forward(const std::vector &pcm, std::vector *layer_mean) const { Matrix hidden(static_cast(pcm.size()),1); hidden.data = pcm; const auto dims = config.at("conv_dim"), kernels = config.at("conv_kernel"), strides = config.at("conv_stride"); for (size_t layer = 0; layer < dims.size(); ++layer) { @@ -251,11 +283,11 @@ Matrix MertModel::forward(const std::vector &pcm, std::vector *lay return hidden; } -AudioFrames MertModel::audio_frames(const std::vector &pcm) const { +AudioFrames MertModel::context::audio_frames(const std::vector &pcm) const { auto output = forward(normalize(pcm),nullptr); return {std::move(output.data),output.rows,output.cols}; } -AudioFrames MertModel::clamp3_features(const std::vector &pcm) const { +AudioFrames MertModel::context::clamp3_features(const std::vector &pcm) const { if (pcm.size() < 24000) throw std::runtime_error("CLaMP3 MERT extraction requires at least one second of audio"); auto normalized = normalize(pcm); @@ -270,4 +302,15 @@ AudioFrames MertModel::clamp3_features(const std::vector &pcm) const { } return result; } + +// Model-facing entry points preserve the existing internal/public facade. +std::vector MertModel::text(const std::vector &ids) const { + return inference->text(ids); +} +AudioFrames MertModel::audio_frames(const std::vector &pcm) const { + return inference->audio_frames(pcm); +} +AudioFrames MertModel::clamp3_features(const std::vector &pcm) const { + return inference->clamp3_features(pcm); +} } // namespace clap::detail diff --git a/src/models/mert2.cpp b/src/models/mert2.cpp index 158e3b9..103420b 100644 --- a/src/models/mert2.cpp +++ b/src/models/mert2.cpp @@ -1,4 +1,5 @@ #include "models.h" +#include "model-context.h" #include "clap.h" #include @@ -18,11 +19,36 @@ float sigmoid(float value) { } } // namespace +// Execution is kept separate from the validated model/tensor contract below. +struct Mert2Model::context final : ModelContext { + context(const Mert2Model &model, int threads) + : ModelContext(model.loader, threads), + model(model), + config(model.config) {} + + Matrix frontend(const std::vector &pcm) const; + Matrix subsample(Matrix hidden) const; + Matrix attention(const Matrix &hidden, const std::string &prefix) const; + Matrix conformer_convolution(const Matrix &hidden, const std::string &prefix) const; + std::vector text(const std::vector &ids) const override; + AudioFrames audio_frames(const std::vector &pcm) const override; + +private: + const Mert2Model &model; + const nlohmann::json &config; +}; + +std::unique_ptr Mert2Model::build_arch_context(int threads) const { + return std::make_unique(*this, threads); +} + Mert2Model::Mert2Model(const ModelLoader &source, int threads) - : loader(source), ops(source, threads) { + : loader(source) { load_arch_hparams(); load_arch_tensors(); + inference = build_arch_context(threads); } +Mert2Model::~Mert2Model() = default; void Mert2Model::load_arch_hparams() { config = json::parse(loader.metadata("mert2.config")); @@ -124,12 +150,12 @@ int Mert2Model::mel_bins() const { return config.at("num_mel_bins"); } int Mert2Model::sample_rate() const { return config.at("sampling_rate"); } bool Mert2Model::is_waveform_encoder() const { return true; } -std::vector Mert2Model::text(const std::vector &) const { +std::vector Mert2Model::context::text(const std::vector &) const { throw std::runtime_error("CLAP: MERT-v2-FullSong has no text encoder"); } -Matrix Mert2Model::frontend(const std::vector &pcm) const { +Matrix Mert2Model::context::frontend(const std::vector &pcm) const { const int fft_size = config.at("n_fft"), hop = config.at("hop_length"); const int bins = config.at("num_mel_bins"), frequencies = fft_size / 2 + 1; const int frames = static_cast(pcm.size() / static_cast(hop)); @@ -165,7 +191,7 @@ Matrix Mert2Model::frontend(const std::vector &pcm) const { return mel; } -Matrix Mert2Model::subsample(Matrix hidden) const { +Matrix Mert2Model::context::subsample(Matrix hidden) const { const auto channels = config.at("subsampling_channels"); const auto depths = config.at("subsampling_depths"); const float epsilon = config.at("subsampling_layer_norm_eps"); @@ -207,7 +233,7 @@ Matrix Mert2Model::subsample(Matrix hidden) const { return hidden; } -Matrix Mert2Model::attention(const Matrix &hidden, const std::string &prefix) const { +Matrix Mert2Model::context::attention(const Matrix &hidden, const std::string &prefix) const { Matrix query = ops.linear(hidden, prefix + ".query_proj"); Matrix key = ops.linear(hidden, prefix + ".key_proj"); const Matrix value = ops.linear(hidden, prefix + ".value_proj"); @@ -251,7 +277,7 @@ Matrix Mert2Model::attention(const Matrix &hidden, const std::string &prefix) co return ops.linear(attended, prefix + ".out_proj"); } -Matrix Mert2Model::conformer_convolution(const Matrix &hidden, const std::string &prefix) const { +Matrix Mert2Model::context::conformer_convolution(const Matrix &hidden, const std::string &prefix) const { const int width = config.at("hidden_size"), kernel = config.at("conv_depthwise_kernel_size"); auto value = ops.norm(hidden, prefix + ".layer_norm", config.at("layer_norm_eps")); value = ops.conv1d(value, prefix + ".conv_block.1", 1, 1, 0, 1, false); @@ -264,7 +290,7 @@ Matrix Mert2Model::conformer_convolution(const Matrix &hidden, const std::string return ops.conv1d(value, prefix + ".conv_block.6", 1, 1, 0, 1, false); } -AudioFrames Mert2Model::audio_frames(const std::vector &pcm) const { +AudioFrames Mert2Model::context::audio_frames(const std::vector &pcm) const { const size_t maximum = (size_t{1} << 30) / sizeof(float); if (!(pcm.size() >= config.at("minimum_input_samples").get())) throw std::runtime_error("CLAP: MERT2 waveform is shorter than minimum_input_samples"); @@ -292,4 +318,12 @@ AudioFrames Mert2Model::audio_frames(const std::vector &pcm) const { } return {std::move(hidden.data), hidden.rows, hidden.cols}; } + +// Model-facing entry points preserve the existing internal/public facade. +std::vector Mert2Model::text(const std::vector &ids) const { + return inference->text(ids); +} +AudioFrames Mert2Model::audio_frames(const std::vector &pcm) const { + return inference->audio_frames(pcm); +} } // namespace clap::detail diff --git a/src/models/models.h b/src/models/models.h index dbeeddd..cd6068d 100644 --- a/src/models/models.h +++ b/src/models/models.h @@ -2,17 +2,18 @@ #include "model.h" #include "model-loader.h" -#include "model-ops.h" #include namespace clap::detail { // Per-architecture declarations belong here, following llama.cpp/src/models. -// GGUF I/O, tokenizer ownership, generic tensor operations and the public Model -// facade are implemented in the core src/ files, not in this specialization. +// Model specializations own validated configuration, tensor contracts and embedded +// backbones. Their nested contexts (defined in each .cpp) own inference helpers +// and execution state. GGUF I/O, vocabulary and the Model facade stay in core. class ClapModel final : public ModelArchitecture { public: ClapModel(const ModelLoader &loader, int threads); + ~ClapModel() override; std::vector text(const std::vector &ids) const override; AudioFrames audio_frames(const std::vector &pcm) const override; TextParameters text_parameters() const override; @@ -25,15 +26,14 @@ class ClapModel final : public ModelArchitecture { private: friend struct FeatureTestAccess; std::vector encode_features(const std::vector &features, int frames) const; + struct context; + std::unique_ptr build_arch_context(int threads) const override; void load_arch_hparams() override; void load_arch_tensors() const override; - Matrix attention(const Matrix &x, const std::string &prefix, int heads, int tokens, - const std::vector *mask = nullptr, int window = 0, int shift = 0, int grid = 0) const; - std::vector project(const Matrix &pooled, const std::string &prefix) const; const ModelLoader &loader; - TensorOps ops; nlohmann::json config, a, t; + std::unique_ptr inference; }; // CLaMP3 text (XLM-R) and audio (BERT), optionally with a bundled MERT frontend. @@ -54,21 +54,15 @@ class Clamp3Model final : public ModelArchitecture { private: friend struct FeatureTestAccess; std::vector encode_features(const std::vector &features, int frames) const; + struct context; + std::unique_ptr build_arch_context(int threads) const override; void load_arch_hparams() override; void load_arch_tensors() const override; - Matrix encode(const Matrix &embeddings, const std::vector &mask, - const nlohmann::json &branch, const std::string &prefix) const; - Matrix attention(const Matrix &x, const std::vector &mask, - const std::string &prefix, int heads) const; - std::vector chunked_text(const std::vector &ids) const; - std::vector chunked_audio(const Matrix &features) const; - std::vector pool_project(const Matrix &hidden, const std::vector &mask, - const std::string &projection) const; const ModelLoader &loader; - TensorOps ops; nlohmann::json config, audio_config, text_config; std::unique_ptr frontend; + std::unique_ptr inference; }; // Official m-a-p/MERT-v2-FullSong waveform encoder: checkpoint frontend, @@ -76,6 +70,7 @@ class Clamp3Model final : public ModelArchitecture { class Mert2Model final : public ModelArchitecture { public: Mert2Model(const ModelLoader &loader, int threads); + ~Mert2Model() override; std::vector text(const std::vector &ids) const override; AudioFrames audio_frames(const std::vector &pcm) const override; TextParameters text_parameters() const override; @@ -84,16 +79,14 @@ class Mert2Model final : public ModelArchitecture { int sample_rate() const override; bool is_waveform_encoder() const override; private: + struct context; + std::unique_ptr build_arch_context(int threads) const override; void load_arch_hparams() override; void load_arch_tensors() const override; - Matrix frontend(const std::vector &pcm) const; - Matrix subsample(Matrix hidden) const; - Matrix attention(const Matrix &hidden, const std::string &prefix) const; - Matrix conformer_convolution(const Matrix &hidden, const std::string &prefix) const; const ModelLoader &loader; - TensorOps ops; nlohmann::json config; + std::unique_ptr inference; }; // MERT-v1-95M: normalized waveform, HuBERT convolutional frontend and // post-layer-normalized transformer. CLaMP3 pooling is a separate contract. @@ -101,6 +94,7 @@ class MertModel final : public ModelArchitecture { public: MertModel(const ModelLoader &loader, int threads, const std::string &tensor_prefix = "", const std::string &metadata_prefix = ""); + ~MertModel() override; std::vector text(const std::vector &) const override; AudioFrames audio_frames(const std::vector &pcm) const override; AudioFrames clamp3_features(const std::vector &pcm) const override; @@ -110,18 +104,14 @@ class MertModel final : public ModelArchitecture { int sample_rate() const override { return 24000; } bool is_waveform_encoder() const override { return true; } private: + struct context; + std::unique_ptr build_arch_context(int threads) const override; void load_arch_hparams() override; void load_arch_tensors() const override; - std::vector normalize(const std::vector &pcm) const; - Matrix forward(const std::vector &normalized, std::vector *layer_mean) const; - Matrix convolution(const Matrix &x, const std::string &name, int kernel, - int stride, int padding, int groups, bool bias) const; - Matrix linear(const Matrix &x, const std::string &name) const; - Matrix attention(const Matrix &x, const std::string &name) const; ModelLoader loader; - TensorOps ops; nlohmann::json config; int minimum_samples = 0; + std::unique_ptr inference; }; // OpenMuQ/MuQ-large-msd-iter: native log-mel, residual 2D subsampling and @@ -129,6 +119,7 @@ class MertModel final : public ModelArchitecture { class MuqModel final : public ModelArchitecture { public: MuqModel(const ModelLoader &loader, int threads); + ~MuqModel() override; std::vector text(const std::vector &) const override; AudioFrames audio_frames(const std::vector &pcm) const override; TextParameters text_parameters() const override; @@ -137,24 +128,20 @@ class MuqModel final : public ModelArchitecture { int sample_rate() const override; bool is_waveform_encoder() const override; private: - struct Image { int frequencies; int frames; Matrix values; }; + struct context; + std::unique_ptr build_arch_context(int threads) const override; void load_arch_hparams() override; void load_arch_tensors() const override; - Matrix frontend(const std::vector &pcm) const; - Matrix subsample(const Matrix &mel) const; - Image conv2d(const Image &input, const std::string &prefix, int stride) const; - void batch_norm(Matrix &input, const std::string &prefix) const; - Matrix attention(const Matrix &hidden, const std::string &prefix) const; - Matrix conformer_convolution(const Matrix &hidden, const std::string &prefix) const; const ModelLoader &loader; - TensorOps ops; nlohmann::json config, encoder; + std::unique_ptr inference; }; // Joint MuQ-MuLan embeddings: the audio backbone remains the exact MuqModel. class MuqMulanModel final : public ModelArchitecture { public: MuqMulanModel(const ModelLoader &loader, int threads); + ~MuqMulanModel() override; std::vector text(const std::vector &ids) const override; AudioFrames audio_frames(const std::vector &) const override; std::vector waveform_embedding(const std::vector &pcm, size_t crop_offset = 0) const override; @@ -165,15 +152,13 @@ class MuqMulanModel final : public ModelArchitecture { bool is_waveform_encoder() const override { return false; } bool has_waveform_embeddings() const override { return true; } private: + struct context; + std::unique_ptr build_arch_context(int threads) const override; void load_arch_hparams() override; void load_arch_tensors() const override; - Matrix encode_text(const Matrix &x) const; - Matrix adapter(Matrix x) const; - Matrix biasless_norm(const Matrix &x, const std::string &name) const; - std::vector project(const Matrix &x, const std::string &name) const; const ModelLoader &loader; - TensorOps ops; MuqModel audio_backbone; nlohmann::json config, text_config; + std::unique_ptr inference; }; } // namespace clap::detail diff --git a/src/models/muq.cpp b/src/models/muq.cpp index 5ebb5e6..2b5817b 100644 --- a/src/models/muq.cpp +++ b/src/models/muq.cpp @@ -1,4 +1,5 @@ #include "models.h" +#include "model-context.h" #include "clap.h" #include #include @@ -19,9 +20,40 @@ float sigmoid(float value) { void silu(Matrix &x) { for (float &v : x.data) v *= sigmoid(v); } } -MuqModel::MuqModel(const ModelLoader &source, int threads) : loader(source), ops(source, threads) { +// Execution is kept separate from the validated model/tensor contract below. +struct MuqModel::context final : ModelContext { + context(const MuqModel &model, int threads) + : ModelContext(model.loader, threads), + model(model), + config(model.config), + encoder(model.encoder) {} + + struct Image { int frequencies; int frames; Matrix values; }; + Image conv2d(const Image &input, const std::string &prefix, int stride) const; + void batch_norm(Matrix &input, const std::string &prefix) const; + Matrix frontend(const std::vector &pcm) const; + Matrix subsample(const Matrix &mel) const; + Matrix attention(const Matrix &hidden, const std::string &prefix) const; + Matrix conformer_convolution(const Matrix &hidden, const std::string &prefix) const; + std::vector text(const std::vector &) const override; + AudioFrames audio_frames(const std::vector &pcm) const override; + +private: + const MuqModel &model; + const nlohmann::json &config; + const nlohmann::json &encoder; +}; + +std::unique_ptr MuqModel::build_arch_context(int threads) const { + return std::make_unique(*this, threads); +} + +MuqModel::MuqModel(const ModelLoader &source, int threads) : loader(source) { load_arch_hparams(); load_arch_tensors(); + inference = build_arch_context(threads); } +MuqModel::~MuqModel() = default; + void MuqModel::load_arch_hparams() { config = nlohmann::json::parse(loader.metadata("muq.config")); encoder = config.at("w2v2_config"); @@ -111,10 +143,10 @@ int MuqModel::projection_dimension() const { return config.at("encoder_dim"); } int MuqModel::mel_bins() const { return 128; } int MuqModel::sample_rate() const { return 24000; } bool MuqModel::is_waveform_encoder() const { return true; } -std::vector MuqModel::text(const std::vector &) const { +std::vector MuqModel::context::text(const std::vector &) const { throw std::runtime_error("CLAP: MuQ has no text encoder"); } -Matrix MuqModel::frontend(const std::vector &pcm) const { +Matrix MuqModel::context::frontend(const std::vector &pcm) const { const int frames = static_cast(pcm.size() / 240); Matrix mel(frames, 128); const auto *window = loader.weight("frontend.window"), *filters = loader.weight("frontend.filters"); @@ -139,7 +171,7 @@ Matrix MuqModel::frontend(const std::vector &pcm) const { } return mel; } -void MuqModel::batch_norm(Matrix &x, const std::string &prefix) const { +void MuqModel::context::batch_norm(Matrix &x, const std::string &prefix) const { const auto *weight = loader.weight(prefix + ".weight"), *bias = loader.weight(prefix + ".bias"); const auto *mean = loader.weight(prefix + ".running_mean"), *variance = loader.weight(prefix + ".running_var"); for (int column = 0; column < x.cols; ++column) { @@ -148,7 +180,7 @@ void MuqModel::batch_norm(Matrix &x, const std::string &prefix) const { x[row][column] = (x[row][column] - scalar(mean, column)) * scale + scalar(bias, column); } } -MuqModel::Image MuqModel::conv2d(const Image &input, const std::string &prefix, int stride) const { +MuqModel::context::Image MuqModel::context::conv2d(const Image &input, const std::string &prefix, int stride) const { const auto *weight = loader.weight(prefix + ".weight"), *bias = loader.weight(prefix + ".bias"); const int frequencies = (input.frequencies + stride - 1) / stride; const int frames = (input.frames + stride - 1) / stride; @@ -192,7 +224,7 @@ MuqModel::Image MuqModel::conv2d(const Image &input, const std::string &prefix, } return result; } -Matrix MuqModel::subsample(const Matrix &mel) const { +Matrix MuqModel::context::subsample(const Matrix &mel) const { Image value{128, mel.rows, Matrix(128 * mel.rows, 1)}; for (int f = 0; f < 128; ++f) for (int t = 0; t < mel.rows; ++t) value.values[f * mel.rows + t][0] = mel[t][f]; for (int stage = 0; stage < 2; ++stage) { @@ -210,7 +242,7 @@ Matrix MuqModel::subsample(const Matrix &mel) const { flattened[t][c * value.frequencies + f] = value.values[f * value.frames + t][c]; return ops.linear(flattened, "conv.linear"); } -Matrix MuqModel::attention(const Matrix &hidden, const std::string &prefix) const { +Matrix MuqModel::context::attention(const Matrix &hidden, const std::string &prefix) const { const int heads = encoder.at("num_attention_heads"), dimension = hidden.cols / heads, half = dimension / 2; const float base = encoder.at("rotary_embedding_base"); Matrix rotated = hidden; @@ -245,7 +277,7 @@ Matrix MuqModel::attention(const Matrix &hidden, const std::string &prefix) cons } return ops.linear(attended, prefix + ".linear_out"); } -Matrix MuqModel::conformer_convolution(const Matrix &hidden, const std::string &prefix) const { +Matrix MuqModel::context::conformer_convolution(const Matrix &hidden, const std::string &prefix) const { const int width = hidden.cols, kernel = encoder.at("conv_depthwise_kernel_size"); auto value = ops.norm(hidden, prefix + ".layer_norm", 1e-5f); value = ops.conv1d(value, prefix + ".pointwise_conv1", 1, 1, 0, 1, false); @@ -256,7 +288,7 @@ Matrix MuqModel::conformer_convolution(const Matrix &hidden, const std::string & batch_norm(value, prefix + ".batch_norm"); silu(value); return ops.conv1d(value, prefix + ".pointwise_conv2", 1, 1, 0, 1, false); } -AudioFrames MuqModel::audio_frames(const std::vector &pcm) const { +AudioFrames MuqModel::context::audio_frames(const std::vector &pcm) const { if (!(pcm.size() >= 1025)) throw std::runtime_error("CLAP: MuQ waveform must contain at least 1025 samples"); if (!(pcm.size() <= (size_t{1} << 30) / sizeof(float))) @@ -282,4 +314,12 @@ AudioFrames MuqModel::audio_frames(const std::vector &pcm) const { hidden = ops.norm(hidden, "conformer.layer_norm", encoder.at("layer_norm_eps")); return {std::move(hidden.data), hidden.rows, hidden.cols}; } + +// Model-facing entry points preserve the existing internal/public facade. +std::vector MuqModel::text(const std::vector &ids) const { + return inference->text(ids); +} +AudioFrames MuqModel::audio_frames(const std::vector &pcm) const { + return inference->audio_frames(pcm); +} } // namespace clap::detail diff --git a/src/models/muq_mulan.cpp b/src/models/muq_mulan.cpp index 9576808..01a930b 100644 --- a/src/models/muq_mulan.cpp +++ b/src/models/muq_mulan.cpp @@ -1,4 +1,5 @@ #include "models.h" +#include "model-context.h" #include #include #include @@ -11,10 +12,42 @@ void positive(const nlohmann::json &obj, const char *key) { throw std::runtime_error(std::string("CLAP: invalid MuLan dimension: ") + key); } } +// Execution is kept separate from the validated model/tensor contract below. +struct MuqMulanModel::context final : ModelContext { + context(const MuqMulanModel &model, int threads) + : ModelContext(model.loader, threads), + model(model), + config(model.config), + text_config(model.text_config), + audio_backbone(model.audio_backbone) {} + + int projection_dimension() const { return model.projection_dimension(); } + Matrix encode_text(const Matrix &x) const; + Matrix adapter(Matrix x) const; + Matrix biasless_norm(const Matrix &x, const std::string &name) const; + std::vector project(const Matrix &x, const std::string &name) const; + std::vector text(const std::vector &ids) const override; + AudioFrames audio_frames(const std::vector &) const override; + std::vector waveform_embedding(const std::vector &pcm, size_t crop_offset = 0) const override; + +private: + const MuqMulanModel &model; + const nlohmann::json &config; + const nlohmann::json &text_config; + const MuqModel &audio_backbone; +}; + +std::unique_ptr MuqMulanModel::build_arch_context(int threads) const { + return std::make_unique(*this, threads); +} + MuqMulanModel::MuqMulanModel(const ModelLoader &source, int threads) - : loader(source), ops(source, threads), audio_backbone(source, threads) { + : loader(source), audio_backbone(source, threads) { load_arch_hparams(); load_arch_tensors(); + inference = build_arch_context(threads); } +MuqMulanModel::~MuqMulanModel() = default; + void MuqMulanModel::load_arch_hparams() { config = nlohmann::json::parse(loader.metadata("muq_mulan.config")); text_config = nlohmann::json::parse(loader.metadata("muq_mulan.text_config")); @@ -93,8 +126,8 @@ TextParameters MuqMulanModel::text_parameters() const { return {0,2,std::numeric_limits::max()}; } int MuqMulanModel::projection_dimension() const { return config.at("mulan").at("dim_latent"); } -AudioFrames MuqMulanModel::audio_frames(const std::vector &) const { throw std::runtime_error("CLAP: MuLan exposes embeddings, not frames"); } -Matrix MuqMulanModel::encode_text(const Matrix &embeddings) const { +AudioFrames MuqMulanModel::context::audio_frames(const std::vector &) const { throw std::runtime_error("CLAP: MuLan exposes embeddings, not frames"); } +Matrix MuqMulanModel::context::encode_text(const Matrix &embeddings) const { const int heads=text_config.at("num_attention_heads"), hd=embeddings.cols/heads; const float eps=text_config.at("layer_norm_eps"); auto x=ops.norm(embeddings,"text.embeddings.LayerNorm",eps); @@ -117,7 +150,7 @@ Matrix MuqMulanModel::encode_text(const Matrix &embeddings) const { } return x; } -Matrix MuqMulanModel::biasless_norm(const Matrix &x,const std::string &name) const { +Matrix MuqMulanModel::context::biasless_norm(const Matrix &x,const std::string &name) const { Matrix y(x.rows,x.cols); const auto *gamma=loader.weight(name+".learned_gamma"); for(int r=0;r();++layer) { const auto p="adapter.layers."+std::to_string(layer); @@ -156,14 +189,14 @@ Matrix MuqMulanModel::adapter(Matrix x) const { } return x; } -std::vector MuqMulanModel::project(const Matrix &x,const std::string &name) const { +std::vector MuqMulanModel::context::project(const Matrix &x,const std::string &name) const { Matrix pooled(1,x.cols);for(int r=0;r MuqMulanModel::text(const std::vector &ids) const { +std::vector MuqMulanModel::context::text(const std::vector &ids) const { const int dim=text_config.at("hidden_size"),positions=text_config.at("max_position_embeddings"),vocab=text_config.at("vocab_size"); if(ids.empty() || ids.size()>static_cast(positions-2)) throw std::runtime_error("CLAP: MuLan text is empty or exceeds XLM-R context; truncation/chunking unsupported"); Matrix x(static_cast(ids.size()),dim);std::vector pos(dim),type(dim); @@ -179,7 +212,7 @@ std::vector MuqMulanModel::text(const std::vector &ids) const { } return project(adapter(encode_text(x)),"text_proj"); } -std::vector MuqMulanModel::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { +std::vector MuqMulanModel::context::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { if (crop_offset != 0) throw std::runtime_error("CLAP: crop offsets are unsupported for this model"); if(pcm.empty() || pcm.size()>(size_t{1}<<28) || !std::all_of(pcm.begin(),pcm.end(),[](float v){return std::isfinite(v);})) throw std::runtime_error("CLAP: MuLan requires finite nonempty waveform of at most 1 GiB"); @@ -199,4 +232,15 @@ std::vector MuqMulanModel::waveform_embedding(const std::vector &p for(float &v:result) v/=count; return result; } + +// Model-facing entry points preserve the existing internal/public facade. +std::vector MuqMulanModel::text(const std::vector &ids) const { + return inference->text(ids); +} +AudioFrames MuqMulanModel::audio_frames(const std::vector &pcm) const { + return inference->audio_frames(pcm); +} +std::vector MuqMulanModel::waveform_embedding(const std::vector &pcm, size_t crop_offset) const { + return inference->waveform_embedding(pcm, crop_offset); +} } // namespace clap::detail