From 7653ac1e857eeb00378066cfcd1b0bd215f0e176 Mon Sep 17 00:00:00 2001 From: co-seven Date: Fri, 21 Aug 2026 09:30:01 +0000 Subject: [PATCH 1/2] feat(server): integrate isolated multi-ASR service --- common/arg.cpp | 7 + common/common.h | 1 + tools/server/server-context.cpp | 91 +++++- tools/smt-mtmd/CMakeLists.txt | 12 + .../smt/multi-asr/multi-asr-common.cpp | 94 ++++++ .../smt-mtmd/smt/multi-asr/multi-asr-common.h | 105 +++++++ .../smt/multi-asr/multi-asr-decoder.cpp | 288 ++++++++++++++++++ .../smt/multi-asr/multi-asr-decoder.h | 67 ++++ .../smt/multi-asr/multi-asr-encoder.cpp | 89 ++++++ .../smt/multi-asr/multi-asr-encoder.h | 36 +++ .../smt/multi-asr/multi-asr-orchestrator.cpp | 188 ++++++++++++ .../smt/multi-asr/multi-asr-orchestrator.h | 86 ++++++ .../smt/multi-asr/multi-asr-service.cpp | 81 +++++ .../smt/multi-asr/multi-asr-service.h | 25 ++ 14 files changed, 1166 insertions(+), 4 deletions(-) create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-common.cpp create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-common.h create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-decoder.cpp create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-decoder.h create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-encoder.cpp create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-encoder.h create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-orchestrator.cpp create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-orchestrator.h create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-service.cpp create mode 100644 tools/smt-mtmd/smt/multi-asr/multi-asr-service.h diff --git a/common/arg.cpp b/common/arg.cpp index 73be78a6a9c..18eb4c05a0c 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -2381,6 +2381,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex params.smt_config_dir = value; } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_MTMD}).set_env("LLAMA_ARG_SMT_CONFIG_DIR")); + add_opt(common_arg( + {"--smt-multi-asr"}, + "enable the isolated legacy multi-ASR FIFO service (default: disabled)", + [](common_params & params) { + params.smt_multi_asr = true; + } + ).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_SMT_MULTI_ASR")); #endif add_opt(common_arg( {"--image", "--audio", "--video"}, "FILE", diff --git a/common/common.h b/common/common.h index 5aa8d46b56d..d8ede71d9b2 100644 --- a/common/common.h +++ b/common/common.h @@ -598,6 +598,7 @@ struct common_params { #if defined(LLAMA_SERVER_SMT_MTMD) std::string media_backend = "auto"; // multimodal backend: auto|mtmd|smt std::string smt_config_dir; // SMT config dir (config.json + ONNX) + bool smt_multi_asr = false; // opt in to the isolated legacy multi-ASR FIFO service #endif std::vector image; // path to image file(s) ; TODO: change the name to "media" int image_min_tokens = -1; diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 585a993f9f1..69f74f30444 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -8,6 +8,9 @@ #include "server-queue.h" #include "server-schema.h" #include "server-stream.h" +#if defined(LLAMA_SERVER_SMT_MTMD) +#include "multi-asr-service.h" +#endif #include "build-info.h" #include "common.h" @@ -894,6 +897,9 @@ struct server_context_impl { server_media_context * media = nullptr; mtmd_context * mctx = nullptr; const llama_vocab * vocab = nullptr; +#if defined(LLAMA_SERVER_SMT_MTMD) + std::unique_ptr smt_asr_service; +#endif server_queue queue_tasks; server_response queue_results; @@ -972,6 +978,11 @@ struct server_context_impl { int64_t t_last_load_progress_ms = 0; void destroy() { +#if defined(LLAMA_SERVER_SMT_MTMD) + // The isolated ASR context borrows model_tgt; tear it down before the + // owning common_init_result releases the model. + smt_asr_service.reset(); +#endif spec.reset(); spec_init.reset(); @@ -1050,8 +1061,10 @@ struct server_context_impl { #if defined(LLAMA_SERVER_SMT_MTMD) const bool has_smt_media = !params.smt_config_dir.empty() && (params.media_backend == "auto" || params.media_backend == "smt"); + const bool use_multi_asr = has_smt_media && !has_mmproj && params.smt_multi_asr; #else const bool has_smt_media = false; + const bool use_multi_asr = false; #endif const bool has_draft = params.speculative.has_dft(); const bool spec_mtp = std::find(params_base.speculative.types.begin(), @@ -1059,6 +1072,24 @@ struct server_context_impl { COMMON_SPECULATIVE_TYPE_DRAFT_MTP) != params_base.speculative.types.end(); const bool has_spec = has_draft || spec_mtp; +#if defined(LLAMA_SERVER_SMT_MTMD) + // Initialize the legacy ASR stack before loading the server's own + // llama model. SpacemiT's backend keeps process-wide state, and the + // proven standalone implementation is sensitive to a second model + // being loaded ahead of it. + if (use_multi_asr && !smt_asr_service) { + try { + smt_asr_service = std::make_unique(); + smt_asr_service->init(nullptr, params_base); + SRV_INF("%s", "initialized isolated SMT multi-ASR service\n"); + } catch (const std::exception & e) { + SRV_ERR("failed to initialize isolated SMT multi-ASR service: %s\n", e.what()); + smt_asr_service.reset(); + return false; + } + } +#endif + if (callback_state) { std::vector stages = {"text_model"}; if (has_spec) { @@ -1078,7 +1109,9 @@ struct server_context_impl { } std::unique_ptr media_worker_init; - if (has_mmproj || has_smt_media) { + // Default SMT audio keeps the original generic media path. Only the + // explicit multi-ASR mode uses the isolated legacy service. + if (has_mmproj || (has_smt_media && !use_multi_asr)) { std::string worker_backend = "mtmd"; #if defined(LLAMA_SERVER_SMT_MTMD) if (params_base.media_backend == "smt" || (params_base.media_backend == "auto" && !has_mmproj && has_smt_media)) { @@ -1248,7 +1281,7 @@ struct server_context_impl { load_progress_callback(1.0f, &load_progress_spec); } - if (has_mmproj || has_smt_media) { + if (has_mmproj || (has_smt_media && !use_multi_asr)) { if (callback_state) { callback_state(SERVER_STATE_LOADING, {{"stage", has_mmproj ? "mmproj_model" : "media_model"}}); } @@ -1534,7 +1567,7 @@ struct server_context_impl { /* chat_template_kwargs */ params_base.default_template_kwargs, /* tmpls */ std::move(chat_templates), /* allow_image */ media ? media->supports_vision() : false, - /* allow_audio */ media ? media->supports_audio() : false, + /* allow_audio */ (media ? media->supports_audio() : false) || smt_asr_service != nullptr, /* allow_video */ media ? media->supports_video() : false, /* enable_thinking */ enable_thinking, /* reasoning_budget */ params_base.sampling.reasoning_budget_tokens, @@ -4030,7 +4063,8 @@ server_context_meta server_context::get_meta() const { /* model_aliases */ impl->model_aliases, /* model_tags */ impl->model_tags, /* model_path */ impl->params_base.model.path, - /* has_mtmd */ impl->media != nullptr && impl->media->supports_prompt_embeddings(), + /* has_mtmd */ (impl->media != nullptr && impl->media->supports_prompt_embeddings()) || + impl->smt_asr_service != nullptr, /* has_inp_image */ impl->chat_params.allow_image, /* has_inp_audio */ impl->chat_params.allow_audio, /* has_inp_video */ impl->chat_params.allow_video, @@ -4116,6 +4150,55 @@ std::unique_ptr server_routes::handle_completions_impl( int32_t sse_ping_interval = params.sse_ping_interval; try { + // Audio-only SMT requests use the isolated legacy ASR orchestrator. + // This synchronous facade is intentionally ahead of task creation: + // no server slot, PEG parser, prompt cache, or shared KV sequence is + // touched by these requests. +#if defined(LLAMA_SERVER_SMT_MTMD) + const std::string raw_prompt = data.contains("prompt") && data.at("prompt").is_string() + ? data.at("prompt").get() : std::string(); + // oaicompat_chat_params_parse() has already decoded input_audio into + // `files`; chat-template rendering may remove the media marker from + // the resulting prompt, so do not use the rendered text to decide + // whether this is an audio request. + const bool has_audio_media = !files.empty(); + if (ctx_server.smt_asr_service && has_audio_media) { + multi_asr_request asr_result; + const int32_t n_predict = json_value(data, "max_tokens", 256); + const std::string user_prompt = raw_prompt; + const bool ok = ctx_server.smt_asr_service->submit(files.front(), user_prompt, n_predict, asr_result); + if (!ok) { + res->error(format_error_response(asr_result.error.empty() ? "ASR request failed" : asr_result.error, + ERROR_TYPE_SERVER)); + return res; + } + const json choice = { + {"index", 0}, + {"message", {{"role", "assistant"}, {"content", asr_result.text}}}, + {"finish_reason", "stop"}, + }; + res->ok({ + {"id", completion_id}, + {"object", "chat.completion"}, + {"created", std::time(nullptr)}, + {"model", meta->model_name}, + {"choices", json::array({choice})}, + {"usage", {{"prompt_tokens", asr_result.timings.n_audio_tokens}, + {"completion_tokens", asr_result.timings.n_out_tokens}, + {"total_tokens", asr_result.timings.n_audio_tokens + asr_result.timings.n_out_tokens}}}, + {"multi_asr_timings", { + {"queue_ms", asr_result.timings.queue_ms}, + {"encode_ms", asr_result.timings.encode_ms}, + {"prefill_ms", asr_result.timings.prefill_ms}, + {"decode_ms", asr_result.timings.decode_ms}, + {"total_ms", asr_result.timings.total_ms}, + {"n_audio_tokens", asr_result.timings.n_audio_tokens}, + {"n_out_tokens", asr_result.timings.n_out_tokens}, + }}, + }); + return res; + } +#endif std::vector tasks; const auto & prompt = data.at("prompt"); diff --git a/tools/smt-mtmd/CMakeLists.txt b/tools/smt-mtmd/CMakeLists.txt index 017ffeb553b..4cb795a1a5f 100644 --- a/tools/smt-mtmd/CMakeLists.txt +++ b/tools/smt-mtmd/CMakeLists.txt @@ -45,6 +45,16 @@ if(LLAMA_SERVER_SMT_MTMD) target_sources(${TARGET} PRIVATE smt/smt-audio-wrapper.cpp smt/smt-audio-wrapper.h + smt/multi-asr/multi-asr-common.cpp + smt/multi-asr/multi-asr-common.h + smt/multi-asr/multi-asr-encoder.cpp + smt/multi-asr/multi-asr-encoder.h + smt/multi-asr/multi-asr-decoder.cpp + smt/multi-asr/multi-asr-decoder.h + smt/multi-asr/multi-asr-orchestrator.cpp + smt/multi-asr/multi-asr-orchestrator.h + smt/multi-asr/multi-asr-service.cpp + smt/multi-asr/multi-asr-service.h smt/smt-media-common.cpp smt/smt-media-common.h smt/smt-profile.h @@ -64,8 +74,10 @@ if(LLAMA_SERVER_SMT_MTMD) ) target_include_directories(${TARGET} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/smt + ${CMAKE_CURRENT_SOURCE_DIR}/smt/multi-asr ) target_include_directories(${TARGET} PRIVATE + ${CMAKE_SOURCE_DIR}/common ${CMAKE_SOURCE_DIR}/tools/mtmd ${CMAKE_SOURCE_DIR}/vendor ${CMAKE_SOURCE_DIR}/ggml/src diff --git a/tools/smt-mtmd/smt/multi-asr/multi-asr-common.cpp b/tools/smt-mtmd/smt/multi-asr/multi-asr-common.cpp new file mode 100644 index 00000000000..034b39bbee3 --- /dev/null +++ b/tools/smt-mtmd/smt/multi-asr/multi-asr-common.cpp @@ -0,0 +1,94 @@ +#include "multi-asr-common.h" + +#include +#include +#include +#include +#include +#include + +// Parse one segment: either a single core "8" or a dash range "8-11". +// Returns true on success and sets the corresponding mask bits. +static bool parse_one_segment(const std::string & seg, bool (&mask)[GGML_MAX_N_THREADS]) { + if (seg.empty()) { + return false; + } + + const size_t dash = seg.find('-'); + if (dash == std::string::npos) { + // single cpu index + char * endp = nullptr; + const long cpu = std::strtol(seg.c_str(), &endp, 10); + if (endp == seg.c_str() || *endp != '\0' || cpu < 0 || cpu >= GGML_MAX_N_THREADS) { + return false; + } + mask[cpu] = true; + return true; + } + + // dash range "lo-hi" + const std::string lo_s = seg.substr(0, dash); + const std::string hi_s = seg.substr(dash + 1); + char * e1 = nullptr; + char * e2 = nullptr; + const long lo = std::strtol(lo_s.c_str(), &e1, 10); + const long hi = std::strtol(hi_s.c_str(), &e2, 10); + if (e1 == lo_s.c_str() || *e1 != '\0' || e2 == hi_s.c_str() || *e2 != '\0') { + return false; + } + if (lo < 0 || hi < 0 || lo >= GGML_MAX_N_THREADS || hi >= GGML_MAX_N_THREADS || lo > hi) { + return false; + } + for (long c = lo; c <= hi; ++c) { + mask[c] = true; + } + return true; +} + +int multi_asr_parse_cpu_range(const std::string & range, bool (&mask)[GGML_MAX_N_THREADS]) { + std::memset(mask, 0, sizeof(bool) * GGML_MAX_N_THREADS); + if (range.empty()) { + return 0; + } + + // Split on commas; each part is a single cpu or a dash range. + std::stringstream ss(range); + std::string part; + while (std::getline(ss, part, ',')) { + // trim ASCII whitespace + size_t b = 0; + size_t e = part.size(); + while (b < e && std::isspace((unsigned char) part[b])) { + ++b; + } + while (e > b && std::isspace((unsigned char) part[e - 1])) { + --e; + } + const std::string seg = part.substr(b, e - b); + if (seg.empty()) { + continue; + } + if (!parse_one_segment(seg, mask)) { + return -1; + } + } + + int count = 0; + for (int i = 0; i < GGML_MAX_N_THREADS; ++i) { + count += mask[i] ? 1 : 0; + } + return count; +} + +std::string multi_asr_cpu_mask_to_string(const bool (&mask)[GGML_MAX_N_THREADS]) { + std::string out; + for (int i = 0; i < GGML_MAX_N_THREADS; ++i) { + if (mask[i]) { + if (!out.empty()) { + out += ","; + } + out += std::to_string(i); + } + } + return out; +} diff --git a/tools/smt-mtmd/smt/multi-asr/multi-asr-common.h b/tools/smt-mtmd/smt/multi-asr/multi-asr-common.h new file mode 100644 index 00000000000..50ea6d21eb3 --- /dev/null +++ b/tools/smt-mtmd/smt/multi-asr/multi-asr-common.h @@ -0,0 +1,105 @@ +#pragma once + +// Multi-ASR 4-way concurrent orchestrator — common types & config. +// +// Design (see docs/../multi-asr-design.md, scheme B1 "错峰流水"): +// - encoder stage : 1 ONNX session (smt_audio_context), pinned to encoder cores, +// serial encode -> produces audio embedding chunk. +// - decoder stage : 1 llama_context slot, pinned to decoder cores, serial decode. +// - orchestrator : FIFO queue (>N waits) + encode/decode threads that overlap +// E(N+1) with D(N). +// Core split (encoder cores vs decoder cores) is a RUNTIME INPUT, not hardcoded, +// so the optimal ratio can be swept experimentally. + +#include "ggml.h" // GGML_MAX_N_THREADS +#include "llama.h" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +struct multi_asr_params { + // model / backend + std::string model_path; // gguf text model (decoder) + std::string smt_config_dir; // dir with config.json + encoder ONNX + bool warmup = true; + + // HTTP + std::string host = "0.0.0.0"; + int32_t port = 8080; + + // concurrency + int32_t n_parallel = 4; // max in-flight requests before queueing (>N waits) + int32_t queue_max = 64; // hard cap on total (in-flight + waiting); reject beyond + + // --- core split (the tunable input variable) --- + // encoder cores: set via the ONNX EP fields in config.json (spacemit_ep_*). + // decoder cores: applied to the llama_context ggml thread affinity (cpumask). + // Accepts a CPU range string like "10,11,14,15" or "12-15". Empty = leave backend default. + std::string decoder_cpu_range; // e.g. "10,11,14,15" or "12-15" + int32_t decoder_n_threads = 0; // 0 = derive from decoder_cpu_range size + + // generation + int32_t n_predict = 256; // max output tokens per request + int32_t n_ctx = 0; // 0 = from model / config.json context_size + int32_t n_batch = 2048; + + // pipeline + bool enable_pipeline = true; // false = strict serial baseline (variable ②) +}; + +// --------------------------------------------------------------------------- +// Per-request lifecycle +// --------------------------------------------------------------------------- + +enum class multi_asr_stage : uint8_t { + queued = 0, + encoding = 1, + decoding = 2, + done = 3, + failed = 4, +}; + +// Per-stage timing (ms), filled as the request flows through the pipeline. +struct multi_asr_timings { + double queue_ms = 0.0; // time spent waiting in FIFO before encode start + double encode_ms = 0.0; // ONNX encoder wall time + double prefill_ms = 0.0; // audio embedding prefill into gguf + double decode_ms = 0.0; // token generation + double total_ms = 0.0; // end-to-end (accept -> response) + int32_t n_audio_tokens = 0; + int32_t n_out_tokens = 0; +}; + +// A single ASR request as it moves through encode -> decode. +struct multi_asr_request { + uint64_t id = 0; + std::vector audio; // raw wav bytes (decoded input) + std::string prompt; // text prompt, e.g. "language Chinese" + int32_t n_predict = 256; + + // filled by encoder stage: audio embedding (n_audio_tokens * hidden_size floats) + std::vector embd; + int32_t n_audio_tokens = 0; + + // result + std::string text; + multi_asr_stage stage = multi_asr_stage::queued; + std::string error; + multi_asr_timings timings; +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +// Parse a CPU range string ("8,9,12,13" or "8-11") into a boolean affinity mask. +// Returns the number of selected CPUs, or -1 on parse error. +int multi_asr_parse_cpu_range(const std::string & range, bool (&mask)[GGML_MAX_N_THREADS]); + +// Convert a boolean affinity mask back to a compact "8,9,12,13" string (for logging). +std::string multi_asr_cpu_mask_to_string(const bool (&mask)[GGML_MAX_N_THREADS]); diff --git a/tools/smt-mtmd/smt/multi-asr/multi-asr-decoder.cpp b/tools/smt-mtmd/smt/multi-asr/multi-asr-decoder.cpp new file mode 100644 index 00000000000..f685156428e --- /dev/null +++ b/tools/smt-mtmd/smt/multi-asr/multi-asr-decoder.cpp @@ -0,0 +1,288 @@ +#include "multi-asr-decoder.h" + +#include "ggml.h" +#include "sampling.h" + +#include +#include +#include +#include + +// Build the Qwen3-ASR prompt around the audio, mirroring +// format_qwen3asr_audio_prompt() in tools/mtmd/mtmd-cli-smt.cpp: +// <|im_start|>system\n<|im_end|>\n<|im_start|>user\n