feat(mvp-system): add toml config overlay and orchestrator cli args
- Layer TOML + env + CLI via config::TomlConfigOverlay (from_layers_with_path_and_args); resolve and validate VastAI config. - Generate provider/cached-model/gguf/vastai CLI args for the orchestrator child; grow mvp_one_node_chat and worker_node. - Adjust relay provisioning. Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
parent
6f1c048669
commit
1ab6264ca0
5 changed files with 1271 additions and 338 deletions
|
|
@ -24,7 +24,6 @@ const DEFAULT_RPC_ADDR: &str = "127.0.0.1:19777";
|
|||
const DEFAULT_NODE_IMAGE: &str = "swactor-mvp-node:latest";
|
||||
const BASE_NODE_IMAGE: &str = "swactor-mvp-node-base:cuda12.6";
|
||||
const MVP_RUNTIME_CONFIG_ENV: &str = "MVP_RUNTIME_CONFIG";
|
||||
const CACHED_MODEL_HOST_ENV: &str = "MVP_CACHED_MODEL_HOST_PATH";
|
||||
const DEFAULT_CACHED_MODEL_FILE: &str = "Llama-3.2-1B-Instruct-Q4_K_M.gguf";
|
||||
const REPO_MODEL_CACHE_DIR: &str = ".model-cache";
|
||||
const DEFAULT_MAX_TOKENS: u32 = 64;
|
||||
|
|
@ -138,8 +137,8 @@ impl Config {
|
|||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let args = ParsedArgs::parse(provided_args)?;
|
||||
let loaded = chat_config::Config::load(args.config_path.as_deref())?;
|
||||
let file = loaded.config;
|
||||
let loaded = chat_config::TomlConfigOverlay::load(args.config_path.as_deref())?;
|
||||
let toml = loaded.overlay;
|
||||
let profile = if args.vastai {
|
||||
RuntimeConfigProfile::Deploy
|
||||
} else {
|
||||
|
|
@ -150,59 +149,59 @@ impl Config {
|
|||
} else {
|
||||
provider_from_env(profile)?
|
||||
};
|
||||
let relay_mode = relay_mode_from_sources(file.relay.mode.as_deref())?;
|
||||
let relay_mode = relay_mode_from_sources(toml.relay.mode.as_deref())?;
|
||||
let relay_url = first_non_empty([
|
||||
env_optional("MVP_IROH_RELAY_URL"),
|
||||
env_optional("SWACTOR_IROH_RELAY_URL"),
|
||||
file.relay.url.clone(),
|
||||
toml.relay.url.clone(),
|
||||
]);
|
||||
let node_image = first_non_empty([
|
||||
args.image,
|
||||
env_optional("MVP_NODE_IMAGE"),
|
||||
if args.vastai {
|
||||
file.vastai.image.clone()
|
||||
toml.vastai.image.clone()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
file.image.node.clone(),
|
||||
toml.image.node.clone(),
|
||||
Some(DEFAULT_NODE_IMAGE.to_owned()),
|
||||
])
|
||||
.expect("default image is non-empty");
|
||||
let max_tokens = args
|
||||
.max_tokens
|
||||
.or(env_u32_optional("MVP_PROMPT_MAX_TOKENS")?)
|
||||
.or(file.prompt.max_tokens)
|
||||
.or(toml.prompt.max_tokens)
|
||||
.unwrap_or(DEFAULT_MAX_TOKENS);
|
||||
let dashboard = args
|
||||
.dashboard
|
||||
.or(env_bool_optional("MVP_DASHBOARD")?)
|
||||
.or(file.prompt.dashboard)
|
||||
.or(toml.prompt.dashboard)
|
||||
.unwrap_or(true);
|
||||
let build_image = args
|
||||
.build_image
|
||||
.or(env_bool_optional("MVP_BUILD_NODE_IMAGE")?)
|
||||
.or(file.image.build)
|
||||
.or(toml.image.build)
|
||||
.unwrap_or(true);
|
||||
let push_image = args
|
||||
.push_image
|
||||
.or(env_bool_optional("MVP_PUSH_NODE_IMAGE")?)
|
||||
.or(file.image.push)
|
||||
.or(toml.image.push)
|
||||
.unwrap_or(false);
|
||||
let force_image_refresh = args
|
||||
.force_image_refresh
|
||||
.or(env_bool_optional("MVP_FORCE_NODE_IMAGE_REFRESH")?)
|
||||
.or(file.image.force_refresh)
|
||||
.or(toml.image.force_refresh)
|
||||
.unwrap_or(false);
|
||||
let image_tag = first_non_empty([
|
||||
args.image_tag,
|
||||
env_optional("MVP_NODE_IMAGE_TAG"),
|
||||
file.image.tag.clone(),
|
||||
toml.image.tag.clone(),
|
||||
]);
|
||||
let rpc_addr = first_non_empty([
|
||||
args.rpc_addr,
|
||||
env_optional("MVP_PROMPT_RPC_ADDR"),
|
||||
env_optional("MVP_PROMPT_RPC_BIND"),
|
||||
file.prompt.rpc_addr.clone(),
|
||||
toml.prompt.rpc_addr.clone(),
|
||||
Some(DEFAULT_RPC_ADDR.to_owned()),
|
||||
])
|
||||
.expect("default RPC address is non-empty");
|
||||
|
|
@ -211,23 +210,28 @@ impl Config {
|
|||
return Err("--dump-logs cannot be combined with --datastream-frame-log; use one datastream log destination".to_owned());
|
||||
}
|
||||
(true, None) => Some(PathBuf::from("mvp-chat.log")),
|
||||
(false, explicit) => {
|
||||
explicit.or_else(|| env_optional("MVP_DATASTREAM_FRAME_LOG").map(PathBuf::from))
|
||||
}
|
||||
(false, explicit) => explicit
|
||||
.or_else(|| env_optional("MVP_DATASTREAM_FRAME_LOG").map(PathBuf::from))
|
||||
.or_else(|| toml.observability.datastream_frame_log.clone().map(PathBuf::from)),
|
||||
};
|
||||
let model_id = first_non_empty([env_optional("MVP_MODEL_ID"), file.model.id.clone()]);
|
||||
let model_id = first_non_empty([env_optional("MVP_MODEL_ID"), toml.model.id.clone()]);
|
||||
let gguf_repo =
|
||||
first_non_empty([env_optional("MVP_GGUF_REPO"), file.model.gguf_repo.clone()]);
|
||||
first_non_empty([env_optional("MVP_GGUF_REPO"), toml.model.gguf_repo.clone()]);
|
||||
let gguf_file =
|
||||
first_non_empty([env_optional("MVP_GGUF_FILE"), file.model.gguf_file.clone()]);
|
||||
first_non_empty([env_optional("MVP_GGUF_FILE"), toml.model.gguf_file.clone()]);
|
||||
let gguf_revision = first_non_empty([
|
||||
env_optional("MVP_GGUF_REVISION"),
|
||||
file.model.gguf_revision.clone(),
|
||||
toml.model.gguf_revision.clone(),
|
||||
]);
|
||||
let max_context = env_u32_optional("MVP_MAX_CONTEXT")?.or(file.model.max_context);
|
||||
let max_context = env_u32_optional("MVP_MAX_CONTEXT")?.or(toml.model.max_context);
|
||||
let cached_model = match (args.cached_model, toml.docker.cached_model_host_path.clone()) {
|
||||
(Some(cached_model), _) => Some(cached_model),
|
||||
(None, Some(path)) => Some(CachedModelConfig::from_arg(Some(path))?),
|
||||
(None, None) => None,
|
||||
};
|
||||
let vastai = if args.vastai {
|
||||
Some(resolve_vastai_config(
|
||||
&file.vastai,
|
||||
&toml.vastai,
|
||||
&node_image,
|
||||
relay_url.clone(),
|
||||
)?)
|
||||
|
|
@ -250,7 +254,7 @@ impl Config {
|
|||
image_tag,
|
||||
push_image,
|
||||
force_image_refresh,
|
||||
cached_model: args.cached_model,
|
||||
cached_model,
|
||||
datastream_frame_log,
|
||||
vastai_yes: args.vastai_yes,
|
||||
model_id,
|
||||
|
|
@ -261,6 +265,107 @@ impl Config {
|
|||
vastai,
|
||||
})
|
||||
}
|
||||
|
||||
fn orchestrator_cli_args(&self, image_ref: &str) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"--runtime-config".to_owned(),
|
||||
self.config_profile.as_str().to_owned(),
|
||||
"--provider".to_owned(),
|
||||
self.provider.as_str().to_owned(),
|
||||
"--image".to_owned(),
|
||||
image_ref.to_owned(),
|
||||
"--rpc-bind".to_owned(),
|
||||
self.rpc_addr.clone(),
|
||||
"--max-tokens".to_owned(),
|
||||
self.max_tokens.to_string(),
|
||||
if self.dashboard {
|
||||
"--dashboard".to_owned()
|
||||
} else {
|
||||
"--no-dashboard".to_owned()
|
||||
},
|
||||
];
|
||||
if let Some(relay_url) = &self.relay_url {
|
||||
args.extend(["--relay-url".to_owned(), relay_url.clone()]);
|
||||
}
|
||||
args.extend([
|
||||
"--relay-mode".to_owned(),
|
||||
relay_mode_env_value(&self.relay_mode).to_owned(),
|
||||
]);
|
||||
if let Some(model_id) = &self.model_id {
|
||||
args.extend(["--model-id".to_owned(), model_id.clone()]);
|
||||
}
|
||||
if let Some(repo) = &self.gguf_repo {
|
||||
args.extend(["--gguf-repo".to_owned(), repo.clone()]);
|
||||
}
|
||||
if let Some(file) = &self.gguf_file {
|
||||
args.extend(["--gguf-file".to_owned(), file.clone()]);
|
||||
}
|
||||
if let Some(revision) = &self.gguf_revision {
|
||||
args.extend(["--gguf-revision".to_owned(), revision.clone()]);
|
||||
}
|
||||
if let Some(max_context) = self.max_context {
|
||||
args.extend(["--max-context".to_owned(), max_context.to_string()]);
|
||||
}
|
||||
if let Some(cached_model) = &self.cached_model {
|
||||
args.extend([
|
||||
"--cached-model-host-path".to_owned(),
|
||||
cached_model.host_path.to_string_lossy().to_string(),
|
||||
]);
|
||||
}
|
||||
if let Some(path) = &self.datastream_frame_log {
|
||||
args.extend([
|
||||
"--datastream-frame-log".to_owned(),
|
||||
path.to_string_lossy().to_string(),
|
||||
]);
|
||||
}
|
||||
if let Some(vastai) = &self.vastai {
|
||||
args.extend([
|
||||
"--vastai-api-key".to_owned(),
|
||||
vastai.api_key.clone(),
|
||||
"--vastai-bootstrap-command".to_owned(),
|
||||
vastai.bootstrap_command.clone(),
|
||||
"--no-vastai-confirm-lease".to_owned(),
|
||||
]);
|
||||
if let Some(disk_gb) = vastai.disk_gb {
|
||||
args.extend(["--vastai-disk-gb".to_owned(), disk_gb.to_string()]);
|
||||
}
|
||||
if let Some(gpu_name) = &vastai.gpu_name {
|
||||
args.extend(["--vastai-gpu-name".to_owned(), gpu_name.clone()]);
|
||||
}
|
||||
if let Some(min_gpu_ram_mb) = vastai.min_gpu_ram_mb {
|
||||
args.extend([
|
||||
"--vastai-min-gpu-ram-mb".to_owned(),
|
||||
min_gpu_ram_mb.to_string(),
|
||||
]);
|
||||
}
|
||||
if let Some(min_down_mbps) = vastai.min_down_mbps {
|
||||
args.extend(["--vastai-min-down-mbps".to_owned(), min_down_mbps.to_string()]);
|
||||
}
|
||||
if let Some(min_up_mbps) = vastai.min_up_mbps {
|
||||
args.extend(["--vastai-min-up-mbps".to_owned(), min_up_mbps.to_string()]);
|
||||
}
|
||||
if let Some(min_reliability) = vastai.min_reliability {
|
||||
args.extend([
|
||||
"--vastai-min-reliability".to_owned(),
|
||||
min_reliability.to_string(),
|
||||
]);
|
||||
}
|
||||
if let Some(require_verified) = vastai.require_verified {
|
||||
args.push(if require_verified {
|
||||
"--vastai-require-verified".to_owned()
|
||||
} else {
|
||||
"--no-vastai-require-verified".to_owned()
|
||||
});
|
||||
}
|
||||
if let Some(onstart) = &vastai.onstart {
|
||||
args.extend(["--vastai-onstart".to_owned(), onstart.clone()]);
|
||||
}
|
||||
if let Some(ssh_identity) = &vastai.ssh_identity {
|
||||
args.extend(["--vastai-ssh-identity".to_owned(), ssh_identity.clone()]);
|
||||
}
|
||||
}
|
||||
args
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -660,82 +765,13 @@ struct OrchChild {
|
|||
impl OrchChild {
|
||||
fn spawn(config: &Config, image_ref: &str) -> Result<Self, String> {
|
||||
let mut command = Command::new(&config.orch_bin);
|
||||
let mut orch_args = config.orchestrator_cli_args(image_ref);
|
||||
orch_args.extend(config.orch_args.clone());
|
||||
command
|
||||
.args(&config.orch_args)
|
||||
.env("MVP_NODE_PROVIDER", config.provider.as_str())
|
||||
.env(MVP_RUNTIME_CONFIG_ENV, config.config_profile.as_str())
|
||||
.env("MVP_NODE_IMAGE", image_ref)
|
||||
.env(
|
||||
"MVP_IROH_RELAY_MODE",
|
||||
relay_mode_env_value(&config.relay_mode),
|
||||
)
|
||||
.env("MVP_PROMPT_RPC_BIND", &config.rpc_addr)
|
||||
.env("MVP_PROMPT_RPC_ADDR", &config.rpc_addr)
|
||||
.env("MVP_PROMPT_MAX_TOKENS", config.max_tokens.to_string())
|
||||
.env("MVP_DASHBOARD", if config.dashboard { "1" } else { "0" })
|
||||
.args(&orch_args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
if let Some(relay_url) = &config.relay_url {
|
||||
command.env("MVP_IROH_RELAY_URL", relay_url);
|
||||
}
|
||||
if let Some(vastai) = &config.vastai {
|
||||
command
|
||||
.env("MVP_VASTAI_API_KEY", &vastai.api_key)
|
||||
.env("MVP_VASTAI_BOOTSTRAP_COMMAND", &vastai.bootstrap_command)
|
||||
.env("MVP_VASTAI_CONFIRM_LEASE", "0");
|
||||
if let Some(disk_gb) = vastai.disk_gb {
|
||||
command.env("MVP_VASTAI_DISK_GB", disk_gb.to_string());
|
||||
}
|
||||
if let Some(gpu_name) = &vastai.gpu_name {
|
||||
command.env("MVP_VASTAI_GPU_NAME", gpu_name);
|
||||
}
|
||||
if let Some(min_gpu_ram_mb) = vastai.min_gpu_ram_mb {
|
||||
command.env("MVP_VASTAI_MIN_GPU_RAM_MB", min_gpu_ram_mb.to_string());
|
||||
}
|
||||
if let Some(min_down_mbps) = vastai.min_down_mbps {
|
||||
command.env("MVP_VASTAI_MIN_DOWN_MBPS", min_down_mbps.to_string());
|
||||
}
|
||||
if let Some(min_up_mbps) = vastai.min_up_mbps {
|
||||
command.env("MVP_VASTAI_MIN_UP_MBPS", min_up_mbps.to_string());
|
||||
}
|
||||
if let Some(min_reliability) = vastai.min_reliability {
|
||||
command.env("MVP_VASTAI_MIN_RELIABILITY", min_reliability.to_string());
|
||||
}
|
||||
if let Some(require_verified) = vastai.require_verified {
|
||||
command.env(
|
||||
"MVP_VASTAI_REQUIRE_VERIFIED",
|
||||
if require_verified { "1" } else { "0" },
|
||||
);
|
||||
}
|
||||
if let Some(onstart) = &vastai.onstart {
|
||||
command.env("MVP_VASTAI_ONSTART", onstart);
|
||||
}
|
||||
if let Some(ssh_identity) = &vastai.ssh_identity {
|
||||
command.env("MVP_VASTAI_SSH_IDENTITY", ssh_identity);
|
||||
}
|
||||
}
|
||||
if let Some(model_id) = &config.model_id {
|
||||
command.env("MVP_MODEL_ID", model_id);
|
||||
}
|
||||
if let Some(repo) = &config.gguf_repo {
|
||||
command.env("MVP_GGUF_REPO", repo);
|
||||
}
|
||||
if let Some(file) = &config.gguf_file {
|
||||
command.env("MVP_GGUF_FILE", file);
|
||||
}
|
||||
if let Some(revision) = &config.gguf_revision {
|
||||
command.env("MVP_GGUF_REVISION", revision);
|
||||
}
|
||||
if let Some(max_context) = config.max_context {
|
||||
command.env("MVP_MAX_CONTEXT", max_context.to_string());
|
||||
}
|
||||
if let Some(cached_model) = &config.cached_model {
|
||||
command.env(CACHED_MODEL_HOST_ENV, &cached_model.host_path);
|
||||
}
|
||||
if let Some(path) = &config.datastream_frame_log {
|
||||
command.env("MVP_DATASTREAM_FRAME_LOG", path);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
unsafe {
|
||||
command.pre_exec(|| {
|
||||
|
|
@ -1484,6 +1520,136 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fn assert_arg_value(args: &[String], flag: &str, expected: &str) {
|
||||
let flag_index = args
|
||||
.iter()
|
||||
.position(|arg| arg == flag)
|
||||
.unwrap_or_else(|| panic!("missing CLI flag {flag}; args={args:?}"));
|
||||
assert_eq!(
|
||||
args.get(flag_index + 1).map(String::as_str),
|
||||
Some(expected),
|
||||
"unexpected value for CLI flag {flag}; args={args:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_flag(args: &[String], flag: &str) {
|
||||
assert!(
|
||||
args.iter().any(|arg| arg == flag),
|
||||
"missing CLI flag {flag}; args={args:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orchestrator_cli_args_cover_wrapper_launch_config() {
|
||||
let config = Config {
|
||||
orch_bin: PathBuf::from("mvp-orchestrator"),
|
||||
orch_args: Vec::new(),
|
||||
rpc_addr: "127.0.0.1:20123".to_owned(),
|
||||
node_image: "docker.io/example/config-node:ignored".to_owned(),
|
||||
config_profile: RuntimeConfigProfile::Local,
|
||||
provider: ProviderKind::Docker,
|
||||
relay_mode: iroh::RelayMode::Default,
|
||||
relay_url: Some("https://relay.example.com".to_owned()),
|
||||
max_tokens: 37,
|
||||
dashboard: false,
|
||||
build_image: false,
|
||||
image_tag: None,
|
||||
push_image: false,
|
||||
force_image_refresh: false,
|
||||
cached_model: Some(CachedModelConfig {
|
||||
host_path: PathBuf::from("/var/cache/swactor/model.gguf"),
|
||||
display_path: PathBuf::from("model.gguf"),
|
||||
}),
|
||||
datastream_frame_log: Some(PathBuf::from("/tmp/mvp-chat-frames.jsonl")),
|
||||
vastai_yes: false,
|
||||
vastai: None,
|
||||
model_id: Some("wrapper-model".to_owned()),
|
||||
gguf_repo: Some("example/wrapper-repo".to_owned()),
|
||||
gguf_file: Some("wrapper-model.gguf".to_owned()),
|
||||
gguf_revision: None,
|
||||
max_context: Some(768),
|
||||
};
|
||||
|
||||
let args = config.orchestrator_cli_args("docker.io/example/prepared-node:latest");
|
||||
|
||||
assert_arg_value(&args, "--runtime-config", "local");
|
||||
assert_arg_value(&args, "--provider", "docker");
|
||||
assert_arg_value(&args, "--image", "docker.io/example/prepared-node:latest");
|
||||
assert_arg_value(&args, "--rpc-bind", "127.0.0.1:20123");
|
||||
assert_arg_value(&args, "--max-tokens", "37");
|
||||
assert_flag(&args, "--no-dashboard");
|
||||
assert_arg_value(&args, "--relay-url", "https://relay.example.com");
|
||||
assert_arg_value(&args, "--model-id", "wrapper-model");
|
||||
assert_arg_value(&args, "--gguf-repo", "example/wrapper-repo");
|
||||
assert_arg_value(&args, "--gguf-file", "wrapper-model.gguf");
|
||||
assert_arg_value(&args, "--max-context", "768");
|
||||
assert_arg_value(
|
||||
&args,
|
||||
"--cached-model-host-path",
|
||||
"/var/cache/swactor/model.gguf",
|
||||
);
|
||||
assert_arg_value(
|
||||
&args,
|
||||
"--datastream-frame-log",
|
||||
"/tmp/mvp-chat-frames.jsonl",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orchestrator_cli_args_cover_vastai_config() {
|
||||
let mut vastai = valid_vastai_config();
|
||||
vastai.onstart = Some("echo preparing vastai node".to_owned());
|
||||
let config = Config {
|
||||
orch_bin: PathBuf::from("mvp-orchestrator"),
|
||||
orch_args: Vec::new(),
|
||||
rpc_addr: DEFAULT_RPC_ADDR.to_owned(),
|
||||
node_image: "ghcr.io/swactor/mvp-node:latest".to_owned(),
|
||||
config_profile: RuntimeConfigProfile::Deploy,
|
||||
provider: ProviderKind::VastAi,
|
||||
relay_mode: iroh::RelayMode::Default,
|
||||
relay_url: Some(vastai.relay_url.clone()),
|
||||
max_tokens: 128,
|
||||
dashboard: false,
|
||||
build_image: false,
|
||||
image_tag: Some("trial".to_owned()),
|
||||
push_image: true,
|
||||
force_image_refresh: false,
|
||||
cached_model: None,
|
||||
datastream_frame_log: None,
|
||||
vastai_yes: true,
|
||||
vastai: Some(vastai),
|
||||
model_id: None,
|
||||
gguf_repo: None,
|
||||
gguf_file: None,
|
||||
gguf_revision: None,
|
||||
max_context: None,
|
||||
};
|
||||
|
||||
let args = config.orchestrator_cli_args("ghcr.io/swactor/mvp-node:latest");
|
||||
|
||||
assert_arg_value(&args, "--vastai-api-key", "vast-key");
|
||||
assert_arg_value(
|
||||
&args,
|
||||
"--vastai-bootstrap-command",
|
||||
"/usr/local/bin/mvp-node",
|
||||
);
|
||||
assert_flag(&args, "--no-vastai-confirm-lease");
|
||||
assert_arg_value(&args, "--vastai-disk-gb", "80");
|
||||
assert_arg_value(&args, "--vastai-gpu-name", "RTX 4090");
|
||||
assert_arg_value(&args, "--vastai-min-gpu-ram-mb", "16000");
|
||||
assert_arg_value(&args, "--vastai-min-down-mbps", "100");
|
||||
assert_arg_value(&args, "--vastai-min-up-mbps", "25");
|
||||
assert_arg_value(&args, "--vastai-min-reliability", "0.98");
|
||||
assert_flag(&args, "--vastai-require-verified");
|
||||
assert_arg_value(&args, "--vastai-onstart", "echo preparing vastai node");
|
||||
assert_arg_value(
|
||||
&args,
|
||||
"--vastai-ssh-identity",
|
||||
"~/.ssh/swactor_vastai_ed25519",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_parser_accepts_only_y_or_yes() {
|
||||
for (input, expected) in [
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -417,18 +417,19 @@ fn run() -> Result<(), String> {
|
|||
"ready",
|
||||
json!({"actor":orchestrator,"source":orchestrator_source}),
|
||||
)?;
|
||||
let node_actor = match stack.runtime.spawn(NodeAgentActor::new(
|
||||
let node_agent = NodeAgentActor::new(
|
||||
stage::NodeId(config.logical_node_id),
|
||||
orchestrator,
|
||||
Some(*reports.addr()),
|
||||
)) {
|
||||
);
|
||||
let node_actor = match stack.runtime.spawn(node_agent) {
|
||||
Ok(actor) => {
|
||||
emit_stdio_node_event(
|
||||
&config,
|
||||
NODE_BOOTSTRAP_CHANNEL,
|
||||
"node_agent",
|
||||
"ready",
|
||||
json!({"node_actor":actor}),
|
||||
json!({"node_actor":actor,"source":"generated"}),
|
||||
)?;
|
||||
actor
|
||||
}
|
||||
|
|
@ -438,7 +439,7 @@ fn run() -> Result<(), String> {
|
|||
NODE_BOOTSTRAP_CHANNEL,
|
||||
"node_agent",
|
||||
"failed",
|
||||
json!({"error":error.to_string()}),
|
||||
json!({"error":error.to_string(),"source":"generated"}),
|
||||
)?;
|
||||
return Err(format!("spawn node agent: {error}"));
|
||||
}
|
||||
|
|
@ -569,7 +570,7 @@ fn run() -> Result<(), String> {
|
|||
.map_err(|e| format!("flush ready line: {e}"))?;
|
||||
|
||||
if let Some(prompt) = &config.self_test_prompt {
|
||||
run_self_test(&mut worker, &config, prompt, &mut datastream)?;
|
||||
run_self_test(&mut worker, &config, prompt, &mut datastream, &mut driver, &stack)?;
|
||||
}
|
||||
|
||||
let shutdown_rx = spawn_stdin_shutdown_listener();
|
||||
|
|
@ -1197,6 +1198,8 @@ fn run_self_test(
|
|||
config: &DeploymentConfig,
|
||||
prompt: &str,
|
||||
datastream: &mut DatastreamEmitter,
|
||||
driver: &mut IrohDriver,
|
||||
stack: &DistributionRuntimeStack,
|
||||
) -> Result<(), String> {
|
||||
emit_node_event(
|
||||
datastream,
|
||||
|
|
@ -1206,7 +1209,7 @@ fn run_self_test(
|
|||
"started",
|
||||
json!({"prompt_bytes":prompt.len()}),
|
||||
);
|
||||
let mut pump = || {};
|
||||
let mut pump = || pump_network(driver, stack);
|
||||
worker.configure_role(
|
||||
config.run_id,
|
||||
config.stage_index,
|
||||
|
|
@ -1664,6 +1667,7 @@ impl Drop for TinygradWorker {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fn spawn_stdin_shutdown_listener() -> Receiver<()> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
thread::spawn(move || {
|
||||
|
|
|
|||
|
|
@ -4,18 +4,40 @@ use std::path::{Path, PathBuf};
|
|||
use serde::Deserialize;
|
||||
use swactor_vastai::SelectionPolicy;
|
||||
|
||||
const DEFAULT_CONFIG_PATH: &str = ".config/config.toml";
|
||||
pub const DEFAULT_CONFIG_PATH: &str = ".config/config.toml";
|
||||
|
||||
/// Transient serde target for `.config/config.toml`.
|
||||
/// This is not runtime state; it exists only to interpret TOML fields and overlay them onto hardcoded defaults.
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub struct TomlConfigOverlay {
|
||||
pub runtime: RuntimeConfigOverlay,
|
||||
pub provider: ProviderConfigOverlay,
|
||||
pub image: ImageConfig,
|
||||
pub relay: RelayConfig,
|
||||
pub prompt: PromptConfig,
|
||||
pub model: ModelConfig,
|
||||
pub docker: DockerConfigOverlay,
|
||||
pub observability: ObservabilityConfigOverlay,
|
||||
pub vastai: VastAiConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct RuntimeConfigOverlay {
|
||||
pub profile: Option<String>,
|
||||
pub run_id: Option<u64>,
|
||||
pub node_id: Option<u64>,
|
||||
pub stage_index: Option<u32>,
|
||||
pub layer_end_exclusive: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct ProviderConfigOverlay {
|
||||
pub kind: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct ImageConfig {
|
||||
|
|
@ -45,12 +67,27 @@ pub struct PromptConfig {
|
|||
#[serde(default)]
|
||||
pub struct ModelConfig {
|
||||
pub id: Option<String>,
|
||||
pub gguf_local_path: Option<String>,
|
||||
pub gguf_repo: Option<String>,
|
||||
pub gguf_file: Option<String>,
|
||||
pub gguf_revision: Option<String>,
|
||||
pub tokenizer_local_path: Option<String>,
|
||||
pub max_context: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct DockerConfigOverlay {
|
||||
pub gpus: Option<String>,
|
||||
pub cached_model_host_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct ObservabilityConfigOverlay {
|
||||
pub datastream_frame_log: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
pub struct VastAiConfig {
|
||||
|
|
@ -58,20 +95,23 @@ pub struct VastAiConfig {
|
|||
pub image: Option<String>,
|
||||
pub bootstrap_command: Option<String>,
|
||||
pub disk_gb: Option<u32>,
|
||||
pub ssh_user: Option<String>,
|
||||
pub confirm_lease: Option<bool>,
|
||||
pub gpu_name: Option<String>,
|
||||
pub min_gpu_ram_mb: Option<u64>,
|
||||
pub min_down_mbps: Option<f64>,
|
||||
pub min_up_mbps: Option<f64>,
|
||||
pub min_reliability: Option<f64>,
|
||||
pub require_verified: Option<bool>,
|
||||
pub poll_interval_secs: Option<u64>,
|
||||
pub onstart: Option<String>,
|
||||
pub ssh_identity: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct LoadedConfig {
|
||||
pub struct LoadedTomlConfigOverlay {
|
||||
pub path: Option<PathBuf>,
|
||||
pub config: Config,
|
||||
pub overlay: TomlConfigOverlay,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
|
@ -91,27 +131,28 @@ pub struct ResolvedVastAiConfig {
|
|||
pub ssh_identity: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(path: Option<&Path>) -> Result<LoadedConfig, String> {
|
||||
impl TomlConfigOverlay {
|
||||
pub fn load(path: Option<&Path>) -> Result<LoadedTomlConfigOverlay, String> {
|
||||
match path {
|
||||
Some(path) => Self::load_required(path).map(|config| LoadedConfig {
|
||||
Some(path) => Self::load_required(path).map(|overlay| LoadedTomlConfigOverlay {
|
||||
path: Some(path.to_path_buf()),
|
||||
config,
|
||||
overlay,
|
||||
}),
|
||||
None => {
|
||||
let default = Path::new(DEFAULT_CONFIG_PATH);
|
||||
if default.is_file() {
|
||||
Self::load_required(default).map(|config| LoadedConfig {
|
||||
path: Some(default.to_path_buf()),
|
||||
config,
|
||||
Self::load_optional(default).map(|overlay| LoadedTomlConfigOverlay {
|
||||
path: overlay.as_ref().map(|_| default.to_path_buf()),
|
||||
overlay: overlay.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_optional(path: &Path) -> Result<Option<Self>, String> {
|
||||
if path.is_file() {
|
||||
Self::load_required(path).map(Some)
|
||||
} else {
|
||||
Ok(LoadedConfig {
|
||||
path: None,
|
||||
config: Self::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,8 +263,19 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn config_toml_parses_explicit_chat_and_vastai_fields() {
|
||||
let config = Config::from_str(
|
||||
let config = TomlConfigOverlay::from_str(
|
||||
r#"
|
||||
|
||||
[runtime]
|
||||
profile = "deploy"
|
||||
run_id = 42
|
||||
node_id = 7
|
||||
stage_index = 2
|
||||
layer_end_exclusive = 24
|
||||
|
||||
[provider]
|
||||
kind = "vastai"
|
||||
|
||||
[image]
|
||||
node = "ghcr.io/swactor/mvp-node:latest"
|
||||
tag = "trial"
|
||||
|
|
@ -243,15 +295,27 @@ dashboard = false
|
|||
|
||||
[model]
|
||||
id = "smollm2-135m-instruct-q4"
|
||||
gguf_local_path = "/models/local.gguf"
|
||||
gguf_repo = "QuantFactory/SmolLM2-135M-Instruct-GGUF"
|
||||
gguf_file = "SmolLM2-135M-Instruct.Q4_K_M.gguf"
|
||||
gguf_revision = "main"
|
||||
tokenizer_local_path = "/models/tokenizer.json"
|
||||
max_context = 512
|
||||
|
||||
[docker]
|
||||
gpus = "all"
|
||||
cached_model_host_path = "/cache/model.gguf"
|
||||
|
||||
[observability]
|
||||
datastream_frame_log = "/tmp/frames.jsonl"
|
||||
|
||||
[vastai]
|
||||
api_key = "vast-key"
|
||||
image = "registry.example.com/team/mvp-node:latest"
|
||||
bootstrap_command = "/opt/mvp/node --join"
|
||||
disk_gb = 80
|
||||
ssh_user = "ubuntu"
|
||||
confirm_lease = true
|
||||
gpu_name = "RTX 4090"
|
||||
min_gpu_ram_mb = 24000
|
||||
min_down_mbps = 250.5
|
||||
|
|
@ -260,10 +324,17 @@ min_reliability = 0.99
|
|||
require_verified = true
|
||||
onstart = "echo preparing"
|
||||
ssh_identity = "~/.ssh/swactor_vastai_ed25519"
|
||||
poll_interval_secs = 30
|
||||
"#,
|
||||
)
|
||||
.expect("explicit config TOML parses");
|
||||
|
||||
assert_eq!(config.runtime.profile.as_deref(), Some("deploy"));
|
||||
assert_eq!(config.runtime.run_id, Some(42));
|
||||
assert_eq!(config.runtime.node_id, Some(7));
|
||||
assert_eq!(config.runtime.stage_index, Some(2));
|
||||
assert_eq!(config.runtime.layer_end_exclusive, Some(24));
|
||||
assert_eq!(config.provider.kind.as_deref(), Some("vastai"));
|
||||
assert_eq!(
|
||||
config.image.node.as_deref(),
|
||||
Some("ghcr.io/swactor/mvp-node:latest")
|
||||
|
|
@ -281,6 +352,10 @@ ssh_identity = "~/.ssh/swactor_vastai_ed25519"
|
|||
assert_eq!(config.prompt.max_tokens, Some(128));
|
||||
assert_eq!(config.prompt.dashboard, Some(false));
|
||||
assert_eq!(config.model.id.as_deref(), Some("smollm2-135m-instruct-q4"));
|
||||
assert_eq!(
|
||||
config.model.gguf_local_path.as_deref(),
|
||||
Some("/models/local.gguf")
|
||||
);
|
||||
assert_eq!(
|
||||
config.model.gguf_repo.as_deref(),
|
||||
Some("QuantFactory/SmolLM2-135M-Instruct-GGUF")
|
||||
|
|
@ -290,7 +365,20 @@ ssh_identity = "~/.ssh/swactor_vastai_ed25519"
|
|||
Some("SmolLM2-135M-Instruct.Q4_K_M.gguf")
|
||||
);
|
||||
assert_eq!(config.model.gguf_revision.as_deref(), Some("main"));
|
||||
assert_eq!(
|
||||
config.model.tokenizer_local_path.as_deref(),
|
||||
Some("/models/tokenizer.json")
|
||||
);
|
||||
assert_eq!(config.model.max_context, Some(512));
|
||||
assert_eq!(config.docker.gpus.as_deref(), Some("all"));
|
||||
assert_eq!(
|
||||
config.docker.cached_model_host_path.as_deref(),
|
||||
Some("/cache/model.gguf")
|
||||
);
|
||||
assert_eq!(
|
||||
config.observability.datastream_frame_log.as_deref(),
|
||||
Some("/tmp/frames.jsonl")
|
||||
);
|
||||
assert_eq!(config.vastai.api_key.as_deref(), Some("vast-key"));
|
||||
assert_eq!(
|
||||
config.vastai.image.as_deref(),
|
||||
|
|
@ -301,12 +389,15 @@ ssh_identity = "~/.ssh/swactor_vastai_ed25519"
|
|||
Some("/opt/mvp/node --join")
|
||||
);
|
||||
assert_eq!(config.vastai.disk_gb, Some(80));
|
||||
assert_eq!(config.vastai.ssh_user.as_deref(), Some("ubuntu"));
|
||||
assert_eq!(config.vastai.confirm_lease, Some(true));
|
||||
assert_eq!(config.vastai.gpu_name.as_deref(), Some("RTX 4090"));
|
||||
assert_eq!(config.vastai.min_gpu_ram_mb, Some(24_000));
|
||||
assert_eq!(config.vastai.min_down_mbps, Some(250.5));
|
||||
assert_eq!(config.vastai.min_up_mbps, Some(50.25));
|
||||
assert_eq!(config.vastai.min_reliability, Some(0.99));
|
||||
assert_eq!(config.vastai.require_verified, Some(true));
|
||||
assert_eq!(config.vastai.poll_interval_secs, Some(30));
|
||||
assert_eq!(config.vastai.onstart.as_deref(), Some("echo preparing"));
|
||||
assert_eq!(
|
||||
config.vastai.ssh_identity.as_deref(),
|
||||
|
|
@ -323,7 +414,7 @@ ssh_identity = "~/.ssh/swactor_vastai_ed25519"
|
|||
));
|
||||
let _ = fs::remove_file(&path);
|
||||
|
||||
let error = Config::load(Some(&path)).expect_err("missing explicit config path errors");
|
||||
let error = TomlConfigOverlay::load(Some(&path)).expect_err("missing explicit config path errors");
|
||||
|
||||
assert!(
|
||||
error.contains(&path.display().to_string()),
|
||||
|
|
|
|||
|
|
@ -126,12 +126,22 @@ impl RelayProvider for StaticRelayProvider {
|
|||
}
|
||||
|
||||
pub fn relay_runtime_config_from_env(run_id: u64) -> Result<RelayRuntimeConfig, String> {
|
||||
match relay_mode_setting_from_env().as_deref() {
|
||||
let mode = relay_mode_setting_from_env();
|
||||
let url = selected_relay_url_from_env();
|
||||
relay_runtime_config_from_settings(run_id, mode.as_deref(), url.as_deref())
|
||||
}
|
||||
|
||||
pub fn relay_runtime_config_from_settings(
|
||||
run_id: u64,
|
||||
mode: Option<&str>,
|
||||
url: Option<&str>,
|
||||
) -> Result<RelayRuntimeConfig, String> {
|
||||
match mode {
|
||||
Some("disabled") => Ok(RelayRuntimeConfig {
|
||||
mode: RelayMode::Disabled,
|
||||
url: None,
|
||||
}),
|
||||
None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id),
|
||||
None | Some("default") => relay_runtime_config_from_optional_static_provider(run_id, url),
|
||||
Some(other) => Err(format!(
|
||||
"unsupported {MVP_IROH_RELAY_MODE_ENV}={other:?}; use disabled or default"
|
||||
)),
|
||||
|
|
@ -151,13 +161,15 @@ pub fn selected_relay_url_from_env() -> Option<String> {
|
|||
|
||||
fn relay_runtime_config_from_optional_static_provider(
|
||||
run_id: u64,
|
||||
url: Option<&str>,
|
||||
) -> Result<RelayRuntimeConfig, String> {
|
||||
let Some(mut provider) = StaticRelayProvider::from_env()? else {
|
||||
let Some(raw_url) = url.map(str::trim).filter(|url| !url.is_empty()) else {
|
||||
return Ok(RelayRuntimeConfig {
|
||||
mode: RelayMode::Default,
|
||||
url: None,
|
||||
});
|
||||
};
|
||||
let mut provider = StaticRelayProvider::from_url_str(raw_url)?;
|
||||
let lease = provider.provision_relay(RelayProvisionRequest {
|
||||
run_id,
|
||||
purpose: RelayPurpose::Combined,
|
||||
|
|
|
|||
Loading…
Reference in a new issue