daemon: embeds through the endpoint pool, GPUs together and the CPU last
query-embed-on-cpu, follow-up daemon-embedder-gpu-pool: huginn-daemon takes repeated --ollama-url (GPU hosts) and --reserve-ollama-url (CPU hosts) and builds the same EmbedderPool as huginn-sources, through one ollama::pool constructor. A lone --ollama-url is a pool of one. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
13647f9220
commit
c74dd530de
5 changed files with 47 additions and 16 deletions
|
|
@ -26,7 +26,9 @@ Upstream: `https://github.com/GameCult/Huginn.git`
|
|||
restart compares the mind with the collection and writes what is missing. An
|
||||
unreachable Qdrant or Ollama shows in `whoami` as the index's status and is
|
||||
retried; admission never waits on it. The daemon requires `--qdrant-url`,
|
||||
`--ollama-url` and `--embedding-model`. A `query` carrying `semantic` is
|
||||
`--ollama-url` and `--embedding-model`; `--ollama-url` repeats for each GPU
|
||||
host and `--reserve-ollama-url` for each CPU host, and the GPU hosts embed
|
||||
together while the CPU hosts serve only when none can. A `query` carrying `semantic` is
|
||||
answered through the same selection as any other: the index supplies
|
||||
candidate ids and scores, and the mind keeps only the documents it holds and
|
||||
the selection admits (ask for `in_force` to exclude resolved ones), ordered by
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ use std::path::PathBuf;
|
|||
use std::process::ExitCode;
|
||||
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use huginn_daemon::index::ollama::OllamaEmbedder;
|
||||
use huginn_daemon::index::pool::EmbedderPool;
|
||||
use huginn_daemon::index::ollama::pool;
|
||||
use huginn_daemon::index::qdrant::QdrantIndex;
|
||||
use huginn_daemon::sources::forge::HttpForge;
|
||||
use huginn_daemon::sources::refresh;
|
||||
|
|
@ -77,8 +76,7 @@ fn main() -> ExitCode {
|
|||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let endpoint = |url: &String| (url.clone(), OllamaEmbedder::new(url, &options.embedding_model));
|
||||
let mut embedder = EmbedderPool::new(options.ollama_urls.iter().map(endpoint).collect(), options.reserve_urls.iter().map(endpoint).collect());
|
||||
let mut embedder = pool(&options.ollama_urls, &options.reserve_urls, &options.embedding_model);
|
||||
let mut index = QdrantIndex::new(&options.qdrant_url);
|
||||
let mut forge = HttpForge::new(&options.forge_url);
|
||||
let mut journal = |report: &huginn_daemon::sources::Report| println!("{report}");
|
||||
|
|
|
|||
|
|
@ -73,3 +73,11 @@ impl Embedder for OllamaEmbedder {
|
|||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// One pool over Ollama endpoints serving `model`: the `primaries` (GPU hosts)
|
||||
/// share the work and the `reserve` (CPU hosts) serve only when none of them
|
||||
/// can. The one embedder client of every Huginn process that embeds.
|
||||
pub fn pool(primaries: &[String], reserve: &[String], model: &str) -> super::pool::EmbedderPool<OllamaEmbedder> {
|
||||
let endpoint = |url: &String| (url.clone(), OllamaEmbedder::new(url, model));
|
||||
super::pool::EmbedderPool::new(primaries.iter().map(endpoint).collect(), reserve.iter().map(endpoint).collect())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ use std::sync::atomic::AtomicBool;
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use huginn_daemon::index::Backoff;
|
||||
use huginn_daemon::index::ollama::OllamaEmbedder;
|
||||
use huginn_daemon::index::ollama::pool;
|
||||
use huginn_daemon::index::qdrant::QdrantIndex;
|
||||
use huginn_daemon::serve::{ServeOptions, parse_options, run, startup};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let options = parse_options(std::env::args().skip(1))?;
|
||||
let embedder = OllamaEmbedder::new(&options.ollama_url, &options.embedding_model);
|
||||
let embedder = pool(&options.ollama_urls, &options.reserve_ollama_urls, &options.embedding_model);
|
||||
let index = QdrantIndex::new(&options.qdrant_url);
|
||||
let (mut daemon, mut hub, registry) = match startup(&options, embedder, index, Backoff::default()) {
|
||||
Ok(started) => started,
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ impl Default for ServeOptions {
|
|||
}
|
||||
}
|
||||
|
||||
/// The six things the operator supplies. Nothing here reads the environment:
|
||||
/// The things the operator supplies. Nothing here reads the environment:
|
||||
/// how Idunn supplies `--bind` is Cut 14's. The index has no off switch: an
|
||||
/// unreachable Qdrant or Ollama is a degraded projection, reported by
|
||||
/// `whoami`, not a configuration.
|
||||
|
|
@ -111,23 +111,34 @@ pub struct Options {
|
|||
pub instance: Slug,
|
||||
pub bind: SocketAddr,
|
||||
pub qdrant_url: String,
|
||||
pub ollama_url: String,
|
||||
/// The GPU hosts: all take embedding at once. One is the plain case.
|
||||
pub ollama_urls: Vec<String>,
|
||||
/// The CPU host: takes embedding only while no GPU host can.
|
||||
pub reserve_ollama_urls: Vec<String>,
|
||||
pub embedding_model: String,
|
||||
}
|
||||
|
||||
/// Exactly `--state-root`, `--instance`, `--bind`, `--qdrant-url`,
|
||||
/// `--ollama-url` and `--embedding-model`, each required, each once. The two
|
||||
/// URLs are plain `http://`: this build carries no TLS.
|
||||
/// `--ollama-url` and `--embedding-model`, each required, each once, except
|
||||
/// `--ollama-url`, which repeats for each GPU host, and the optional
|
||||
/// `--reserve-ollama-url`, which repeats for each CPU host. The URLs are plain
|
||||
/// `http://`: this build carries no TLS.
|
||||
pub fn parse_options(args: impl Iterator<Item = String>) -> Result<Options> {
|
||||
let mut args = args;
|
||||
let mut values: BTreeMap<String, String> = BTreeMap::new();
|
||||
let (mut ollama_urls, mut reserve_ollama_urls) = (Vec::new(), Vec::new());
|
||||
while let Some(name) = args.next() {
|
||||
let name = name.strip_prefix("--").with_context(|| format!("expected --option, got {name:?}"))?;
|
||||
ensure!(
|
||||
matches!(name, "state-root" | "instance" | "bind" | "qdrant-url" | "ollama-url" | "embedding-model"),
|
||||
matches!(name, "state-root" | "instance" | "bind" | "qdrant-url" | "ollama-url" | "reserve-ollama-url" | "embedding-model"),
|
||||
"unsupported Huginn option --{name}"
|
||||
);
|
||||
let value = args.next().with_context(|| format!("missing value for --{name}"))?;
|
||||
if name == "ollama-url" || name == "reserve-ollama-url" {
|
||||
ensure!(value.starts_with("http://"), "--{name} must be a plain http:// address, got {value:?}");
|
||||
if name == "ollama-url" { ollama_urls.push(value) } else { reserve_ollama_urls.push(value) }
|
||||
continue;
|
||||
}
|
||||
ensure!(values.insert(name.to_owned(), value).is_none(), "duplicate Huginn option --{name}");
|
||||
}
|
||||
let take = |name: &str| -> Result<String> {
|
||||
|
|
@ -148,7 +159,11 @@ pub fn parse_options(args: impl Iterator<Item = String>) -> Result<Options> {
|
|||
instance: Slug(take("instance")?),
|
||||
bind: bind.parse().with_context(|| format!("--bind must be an ip:port, got {bind:?}"))?,
|
||||
qdrant_url: url("qdrant-url")?,
|
||||
ollama_url: url("ollama-url")?,
|
||||
ollama_urls: {
|
||||
ensure!(!ollama_urls.is_empty(), "--ollama-url is required; repeat it for each GPU host");
|
||||
ollama_urls
|
||||
},
|
||||
reserve_ollama_urls,
|
||||
embedding_model,
|
||||
})
|
||||
}
|
||||
|
|
@ -1347,7 +1362,8 @@ mod tests {
|
|||
instance: slug(INSTANCE),
|
||||
bind: port,
|
||||
qdrant_url: "http://127.0.0.1:1".into(),
|
||||
ollama_url: "http://127.0.0.1:1".into(),
|
||||
ollama_urls: vec!["http://127.0.0.1:1".into()],
|
||||
reserve_ollama_urls: Vec::new(),
|
||||
embedding_model: "a-model".into(),
|
||||
};
|
||||
let started = |options: &Options| {
|
||||
|
|
@ -1369,7 +1385,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn options_are_exactly_the_six_the_operator_supplies() {
|
||||
fn options_are_the_ones_the_operator_supplies() {
|
||||
let root = if cfg!(windows) { r"C:\state" } else { "/state" };
|
||||
let args = |values: &[&str]| values.iter().map(|value| value.to_string()).collect::<Vec<_>>().into_iter();
|
||||
let good = [
|
||||
|
|
@ -1383,7 +1399,8 @@ mod tests {
|
|||
instance: slug("yggdrasil"),
|
||||
bind: "127.0.0.1:17872".parse().unwrap(),
|
||||
qdrant_url: "http://127.0.0.1:6333".into(),
|
||||
ollama_url: "http://10.77.0.4:11434".into(),
|
||||
ollama_urls: vec!["http://10.77.0.4:11434".into()],
|
||||
reserve_ollama_urls: Vec::new(),
|
||||
embedding_model: "qwen3-embedding:0.6b".into(),
|
||||
}
|
||||
);
|
||||
|
|
@ -1411,5 +1428,11 @@ mod tests {
|
|||
] {
|
||||
assert!(parse_options(args(&bad)).is_err(), "{bad:?}");
|
||||
}
|
||||
// The GPU hosts and the CPU host repeat; a lone --ollama-url is the plain case.
|
||||
let pooled = [good.to_vec(), vec!["--ollama-url", "http://10.77.0.2:11434", "--reserve-ollama-url", "http://127.0.0.1:11434"]].concat();
|
||||
let parsed = parse_options(args(&pooled)).unwrap();
|
||||
assert_eq!(parsed.ollama_urls, ["http://10.77.0.4:11434", "http://10.77.0.2:11434"]);
|
||||
assert_eq!(parsed.reserve_ollama_urls, ["http://127.0.0.1:11434"]);
|
||||
assert!(parse_options(args(&[good.to_vec(), vec!["--reserve-ollama-url", "127.0.0.1:11434"]].concat())).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue