Compare commits

...
4 Commits
Author SHA1 Message Date
Xuan-Son NguyenandGitHub e1470ee6a2 server: (router) do not evict busy models (#26567) 2026-08-07 14:39:59 +02:00
PascalandGitHub 217df17ac3 mtmd: stop feeding the text stream again during Qwen3-TTS generation (#26706)
The reference implementation has two mutually exclusive prompt layouts.
In non streaming mode the prefill carries the whole utterance text plus
tts_eos summed with codec_pad, and the trailing text hidden collapses to
a single tts_pad row. In streaming mode the prefill carries only the
first text token and the trailing rows stream the rest of the text
followed by tts_eos.

The pipeline built the non streaming prefill but the streaming overlay,
so the talker saw the utterance a second time during generation and read
it twice before emitting codec_eos.

The overlay is now the single tts_pad row that matches the prefill.
2026-08-07 13:32:52 +02:00
Kilian HuandGitHub cb26014d96 ggml : add aarch64 HWCAP fallbacks and fix fp16 variant detection (#25554)
* ggml : add fallback definitions for missing aarch64 HWCAP bits

* ggml : require HWCAP_ASIMDHP for the aarch64 fp16 cpu variants

Also rename has_fp16_va to has_fp16, the field gates the whole FEAT_FP16
extension, scalar and vector half-precision arithmetic together.
2026-08-07 14:07:10 +03:00
PascalandGitHub 82bb48500a ui: read model modalities from the router model list (#26709)
* ui: read model modalities from the router model list

The router advertises input modalities for every model, loaded or not.
Reading them at list build time lets the UI accept image and audio
uploads for a model selected through ?model=, which has no /props yet.

* enum
2026-08-07 12:07:58 +02:00
6 changed files with 89 additions and 25 deletions
+19 -3
View File
@@ -8,6 +8,22 @@
#include <sys/sysctl.h>
#endif
#if !defined(HWCAP_FPHP)
#define HWCAP_FPHP (1 << 9)
#endif
#if !defined(HWCAP_ASIMDHP)
#define HWCAP_ASIMDHP (1 << 10)
#endif
#if !defined(HWCAP_ASIMDDP)
#define HWCAP_ASIMDDP (1 << 20)
#endif
#if !defined(HWCAP_SVE)
#define HWCAP_SVE (1 << 22)
#endif
#if !defined(HWCAP2_SVE2)
#define HWCAP2_SVE2 (1 << 1)
#endif
@@ -23,7 +39,7 @@
struct aarch64_features {
// has_neon not needed, aarch64 has NEON guaranteed
bool has_dotprod = false;
bool has_fp16_va = false;
bool has_fp16 = false;
bool has_sve = false;
bool has_sve2 = false;
bool has_i8mm = false;
@@ -36,7 +52,7 @@ struct aarch64_features {
uint32_t hwcap2 = getauxval(AT_HWCAP2);
has_dotprod = !!(hwcap & HWCAP_ASIMDDP);
has_fp16_va = !!(hwcap & HWCAP_FPHP);
has_fp16 = !!(hwcap & HWCAP_FPHP) && !!(hwcap & HWCAP_ASIMDHP);
has_sve = !!(hwcap & HWCAP_SVE);
has_sve2 = !!(hwcap2 & HWCAP2_SVE2);
has_i8mm = !!(hwcap2 & HWCAP2_I8MM);
@@ -75,7 +91,7 @@ static int ggml_backend_cpu_aarch64_score() {
score += 1<<1;
#endif
#ifdef GGML_USE_FP16_VECTOR_ARITHMETIC
if (!af.has_fp16_va) { return 0; }
if (!af.has_fp16) { return 0; }
score += 1<<2;
#endif
#ifdef GGML_USE_SVE
+5 -11
View File
@@ -112,7 +112,6 @@ public:
c2w_state.clear();
audio_pcm.clear();
overlay.clear();
overlay_idx = 0;
h_state_buf.clear();
out_buf.clear();
prompt_embd_buf.clear();
@@ -205,11 +204,9 @@ public:
top_p = inp->top_p > 0 ? inp->top_p : 1.0f;
out_type = inp->out_type;
// the text stream keeps flowing during generation: after frame k, the input adds
// trailing text row k on top of the codes embedding, then tts_eos, then tts_pad
for (int i = 3; i < n_ids - 5; i++) overlay.push_back(row(ids[(size_t) i]));
overlay.push_back(row(tts_eos));
overlay.push_back(row(tts_pad));
// the prompt above holds the whole text stream up to tts_eos, so every generated
// frame adds tts_pad on top of the codes embedding
overlay = row(tts_pad);
return 0;
}
@@ -265,9 +262,7 @@ public:
}
std::vector<float> fb(out.embd, out.embd + n_embd);
const auto & ov = overlay[std::min(overlay_idx, overlay.size() - 1)];
for (int i = 0; i < n_embd; i++) fb[(size_t) i] += ov[(size_t) i];
overlay_idx++;
for (int i = 0; i < n_embd; i++) fb[(size_t) i] += overlay[(size_t) i];
const int n_pos_per_embd = mrope ? 4 : 1;
decode_embd_batch batch_embd(fb.data(), 1, n_pos_per_embd, n_embd);
@@ -437,8 +432,7 @@ private:
std::vector<int32_t> codes_buf;
std::vector<uint8_t> c2w_state;
std::vector<float> audio_pcm;
std::vector<std::vector<float>> overlay;
size_t overlay_idx = 0;
std::vector<float> overlay;
std::vector<float> h_state_buf;
mtmd_helper_gen_audio_outtype out_type = MTMD_HELPER_GEN_AUDIO_OUTTYPE_WAV;
std::vector<char> out_buf;
+23 -8
View File
@@ -721,7 +721,9 @@ void server_models::unload_lru() {
for (const auto & m : mapping) {
if (m.second.meta.is_running()) {
count_active++;
if (m.second.meta.last_used < lru_last_used) {
// do not evict busy one
bool is_model_idle = m.second.req_count == 0 && m.second.meta.is_ready_or_sleep();
if (is_model_idle && m.second.meta.last_used < lru_last_used) {
lru_model_name = m.first;
lru_last_used = m.second.meta.last_used;
}
@@ -739,6 +741,7 @@ void server_models::unload_lru() {
});
}
}
// TODO @ngxson : if no idle model is found, queue the load request
}
void server_models::load(const std::string & name) {
@@ -1180,9 +1183,12 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
if (!meta->is_running()) {
throw std::invalid_argument("model name=" + name + " is not running");
}
if (update_last_used) {
{
std::unique_lock<std::mutex> lk(mutex);
mapping[name].meta.last_used = ggml_time_ms();
if (update_last_used) {
mapping[name].meta.last_used = ggml_time_ms();
}
mapping[name].req_count++;
}
SRV_INF("proxying request to model %s on port %d\n", name.c_str(), meta->port);
std::string proxy_path = req.path;
@@ -1198,13 +1204,22 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
req.headers,
req.body,
req.files,
// a detached request belongs to a replay session that outlives the client socket:
// it reaches the child even when the downstream died during the load wait, the
// session buffer is the recipient and DELETE remains the stop
detached ? std::function<bool()>([]() { return false; }) : req.should_stop,
// a detached request belongs to a replay session
detached
? std::function<bool()>([]() { return false; })
: req.should_stop,
base_params.timeout_read,
base_params.timeout_write
);
proxy->cleanup = [this, name]() {
std::unique_lock<std::mutex> lk(mutex);
auto it = mapping.find(name);
if (it != mapping.end() && it->second.req_count > 0) {
it->second.req_count--;
}
};
return proxy;
}
@@ -2064,7 +2079,7 @@ server_http_proxy::server_http_proxy(
cli->set_write_timeout(timeout_read, 0); // reversed for cli (client) vs srv (server)
cli->set_read_timeout(timeout_write, 0);
this->status = 500; // to be overwritten upon response
this->cleanup = [pipe]() {
this->cleanup_pipes = [pipe]() {
pipe->close_read();
pipe->close_write();
};
+9 -2
View File
@@ -84,7 +84,6 @@ struct server_model_meta {
int exit_code = 0; // exit code of the model instance process (only valid if status == FAILED)
int stop_timeout = 0; // seconds to wait before force-killing the model instance during shutdown
mtmd_caps multimodal; // multimodal capabilities
// bool need_download = false; // whether the model needs to be downloaded before loading // TODO @ngxson: implement this
bool is_ready() const {
return status == SERVER_MODEL_STATUS_LOADED;
@@ -94,6 +93,10 @@ struct server_model_meta {
return status == SERVER_MODEL_STATUS_LOADED || status == SERVER_MODEL_STATUS_LOADING || status == SERVER_MODEL_STATUS_SLEEPING;
}
bool is_ready_or_sleep() const {
return status == SERVER_MODEL_STATUS_LOADED || status == SERVER_MODEL_STATUS_SLEEPING;
}
bool is_failed() const {
return status == SERVER_MODEL_STATUS_UNLOADED && exit_code != 0;
}
@@ -113,6 +116,7 @@ private:
std::shared_ptr<server_subproc> subproc; // shared between main thread and monitoring thread
std::thread th;
server_model_meta meta;
int req_count = 0; // number of active proxy requests
};
std::mutex mutex;
@@ -343,7 +347,6 @@ struct server_models_routes {
*/
struct server_http_proxy : server_http_res {
std::function<void()> cleanup = nullptr;
public:
server_http_proxy(const std::string & method,
const std::string & scheme,
const std::string & host,
@@ -357,11 +360,15 @@ public:
int32_t timeout_write
);
~server_http_proxy() {
if (cleanup_pipes) {
cleanup_pipes();
}
if (cleanup) {
cleanup();
}
}
private:
std::function<void()> cleanup_pipes = nullptr;
std::thread thread;
struct msg_t {
std::map<std::string, std::string> headers;
+22 -1
View File
@@ -1,7 +1,12 @@
import { base } from '$app/paths';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
import { ServerModelStatus, ServerModelsSseEventType, ModelModality } from '$lib/enums';
import {
ServerModelStatus,
ServerModelsSseEventType,
ModelModality,
FileTypeCategory
} from '$lib/enums';
import { ModelsService } from '$lib/services/models.service';
import { PropsService } from '$lib/services/props.service';
import { serverStore, isRouterMode } from '$lib/stores/server.svelte';
@@ -394,6 +399,7 @@ class ModelsStore {
model: modelId,
description: details?.description,
capabilities: rawCapabilities.filter((value: unknown): value is string => Boolean(value)),
modalities: this.buildArchitectureModalities(item.architecture),
details: details?.details,
meta: item.meta ?? null,
parsedId: ModelsService.parseModelId(modelId),
@@ -1000,6 +1006,21 @@ class ModelsStore {
};
}
/** Map the router modalities, the only source available while a model is not loaded. */
private buildArchitectureModalities(
architecture: ApiModelDataEntry['architecture']
): ModelModalities | undefined {
if (!architecture) return undefined;
const inputs = architecture.input_modalities;
return {
vision: inputs.includes(FileTypeCategory.IMAGE),
audio: inputs.includes(FileTypeCategory.AUDIO),
video: inputs.includes(FileTypeCategory.VIDEO)
};
}
clear(): void {
this.unsubscribeStatus();
this.statusWaiters.forEach((waiter) => waiter.reject(new Error('Models store cleared')));
+11
View File
@@ -98,10 +98,21 @@ export interface ApiModelDataEntry {
aliases?: string[];
/** Informational tags for this model */
tags?: string[];
/** Modality capabilities, reported by the router for every model regardless of load state */
architecture?: ApiModelArchitecture;
/** Legacy meta field (may be present in older responses) */
meta?: Record<string, unknown> | null;
}
/**
* Modality capabilities of a model, as advertised by the ROUTER /models endpoint.
* Read from the model manifest, so it is available before the model is loaded.
*/
export interface ApiModelArchitecture {
/** Accepted input modalities, always contains "text" */
input_modalities: string[];
}
/**
* Load stage reported by the /models/sse feed, in load order.
*/