From efa28fd1c8f793b44ced37cb0b66a5d6ed7be077 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Mon, 6 Jul 2026 11:24:36 -0400 Subject: [PATCH 01/14] Add core --- Cargo.lock | 4 + Cargo.toml | 1 + examples/huntsman/nn/core/Cargo.toml | 9 ++ examples/huntsman/nn/core/src/lib.rs | 155 +++++++++++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 examples/huntsman/nn/core/Cargo.toml create mode 100644 examples/huntsman/nn/core/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 326b2b189..bede9c7df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,6 +849,10 @@ dependencies = [ "spider-tdl", ] +[[package]] +name = "huntsman-nn-core" +version = "0.1.0" + [[package]] name = "hyper" version = "1.10.1" diff --git a/Cargo.toml b/Cargo.toml index c624e3e7e..ddd377fcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "components/spider-utils", "examples/huntsman/complex/tasks", "examples/huntsman/complex/types", + "examples/huntsman/nn/core", "tests/huntsman/em-runtime", "tests/huntsman/integration-test-tasks", "tests/huntsman/task-executor", diff --git a/examples/huntsman/nn/core/Cargo.toml b/examples/huntsman/nn/core/Cargo.toml new file mode 100644 index 000000000..f3e4c6791 --- /dev/null +++ b/examples/huntsman/nn/core/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "huntsman-nn-core" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +name = "huntsman_nn_core" +path = "src/lib.rs" \ No newline at end of file diff --git a/examples/huntsman/nn/core/src/lib.rs b/examples/huntsman/nn/core/src/lib.rs new file mode 100644 index 000000000..e8fcd760d --- /dev/null +++ b/examples/huntsman/nn/core/src/lib.rs @@ -0,0 +1,155 @@ +//! Pure neuron math for the Spider end-to-end neural-network test workload. +//! +//! A dense-layer neuron computes `activation(weighted_sum(inputs) + bias)` over a fixed fan-in of +//! 25 scalar `double` inputs. + +/// The fixed neuron fan-in: each neuron consumes exactly this many scalar inputs. +pub const NUM_INPUTS: usize = 25; + +/// The fixed per-input weights, one per input position. Deterministic values calculated as +/// (`WEIGHTS[k] = (k + 1) * 0.01`). +pub const WEIGHTS: [f64; NUM_INPUTS] = [ + 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, + 0.17, 0.18, 0.19, 0.20, 0.21, 0.22, 0.23, 0.24, 0.25, +]; + +/// The fixed bias added to the weighted sum before the activation. +pub const BIAS: f64 = 0.5; + +/// Rectified-linear activation: `max(0.0, x)`. +#[must_use] +pub const fn relu(x: f64) -> f64 { + f64::max(0.0, x) +} + +/// Logistic sigmoid activation: `1.0 / (1.0 + exp(-x))`. +#[must_use] +pub fn sigmoid(x: f64) -> f64 { + 1.0 / (1.0 + f64::exp(-x)) +} + +/// Identity activation: returns its argument unchanged. +#[must_use] +pub const fn identity(x: f64) -> f64 { + x +} + +#[must_use] +pub fn dense_relu(inputs: &[f64; NUM_INPUTS]) -> f64 { + relu(weighted_sum(inputs)) +} + +#[must_use] +pub fn dense_sigmoid(inputs: &[f64; NUM_INPUTS]) -> f64 { + sigmoid(weighted_sum(inputs)) +} + +#[must_use] +pub fn dense_identity(inputs: &[f64; NUM_INPUTS]) -> f64 { + identity(weighted_sum(inputs)) +} + +/// Computes the pre-activation `sum(WEIGHTS[k] * inputs[k])+ BIAS`. +fn weighted_sum(inputs: &[f64; NUM_INPUTS]) -> f64 { + let mut acc = BIAS; + for (w, x) in WEIGHTS.iter().zip(inputs.iter()) { + acc += w * x; + } + acc +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Relative-tolerance float equality used to compare hand-computed and computed values. + fn assert_approx_eq(actual: f64, expected: f64) { + let diff = (actual - expected).abs(); + let tol = 1.0e-12_f64 * (1.0 + expected.abs()); + assert!( + diff <= tol, + "actual={actual}, expected={expected}, diff={diff}, tol={tol}", + ); + } + + #[test] + fn test_relu() { + assert_approx_eq(relu(-1.0), 0.0); + assert_approx_eq(relu(0.0), 0.0); + assert_approx_eq(relu(2.5), 2.5); + } + + #[test] + fn test_sigmoid() { + // sigmoid(0) = 0.5; saturates toward 1 for large positive inputs, toward 0 for large + // negative inputs. For large positive x, exp(-x) underflows relative to 1.0 in `f64`, so + // sigmoid rounds to exactly 1.0. + assert_approx_eq(sigmoid(0.0), 0.5); + assert_approx_eq(sigmoid(100.0), 1.0); + assert_approx_eq(sigmoid(-100.0), 0.0); + // Monotonic non-decreasing across the range. + assert!(sigmoid(-1.0) < sigmoid(0.0)); + assert!(sigmoid(0.0) < sigmoid(1.0)); + } + + #[test] + fn test_identity() { + assert_approx_eq(identity(-3.0), -3.0); + assert_approx_eq(identity(0.0), 0.0); + assert_approx_eq(identity(7.25), 7.25); + } + + #[test] + fn test_weighted_sum_all_zero_inputs_equals_bias() { + let inputs = [0.0_f64; NUM_INPUTS]; + assert_approx_eq(weighted_sum(&inputs), BIAS); + } + + #[test] + fn test_weighted_sum_all_one_inputs() { + // sum_k WEIGHTS[k] = 0.01 * (1 + 2 + ... + 25) = 0.01 * 325 = 3.25; plus BIAS (0.5) = 3.75. + let inputs = [1.0_f64; NUM_INPUTS]; + assert_approx_eq(weighted_sum(&inputs), 3.75); + } + + #[test] + fn test_dense_relu() { + let zero = [0.0_f64; NUM_INPUTS]; + // zero inputs -> pre-activation = BIAS = 0.5 -> relu(0.5) = 0.5. + assert_approx_eq(dense_relu(&zero), 0.5); + + let ones = [1.0_f64; NUM_INPUTS]; + // pre-activation = 3.75 -> relu(3.75) = 3.75. + assert_approx_eq(dense_relu(&ones), 3.75); + + // Negative weighted sum (large negative inputs) clamps to 0 under relu. + let neg = [-1000.0_f64; NUM_INPUTS]; + assert_approx_eq(dense_relu(&neg), 0.0); + } + + #[test] + fn test_dense_sigmoid() { + let zero = [0.0_f64; NUM_INPUTS]; + // zero inputs -> pre-activation = BIAS = 0.5 -> sigmoid(0.5) = 1 / (1 + exp(-0.5)). + assert_approx_eq(dense_sigmoid(&zero), sigmoid(BIAS)); + + let ones = [1.0_f64; NUM_INPUTS]; + // pre-activation = 3.75 -> sigmoid(3.75). + assert_approx_eq(dense_sigmoid(&ones), sigmoid(3.75)); + } + + #[test] + fn test_dense_identity() { + let zero = [0.0_f64; NUM_INPUTS]; + // zero inputs -> pre-activation = BIAS = 0.5 -> identity(0.5) = 0.5. + assert_approx_eq(dense_identity(&zero), 0.5); + + let ones = [1.0_f64; NUM_INPUTS]; + // pre-activation = 3.75 -> identity(3.75) = 3.75. + assert_approx_eq(dense_identity(&ones), 3.75); + + let neg = [-1000.0_f64; NUM_INPUTS]; + // identity preserves the (negative) pre-activation, unlike relu. + assert_approx_eq(dense_identity(&neg), weighted_sum(&neg)); + } +} From 8004dbe194bd375314619141a8a81c7272a0fc64 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Mon, 6 Jul 2026 11:48:00 -0400 Subject: [PATCH 02/14] Add task wrapper --- Cargo.lock | 9 ++ Cargo.toml | 1 + examples/huntsman/nn/tasks/Cargo.toml | 18 ++++ examples/huntsman/nn/tasks/src/lib.rs | 121 ++++++++++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 examples/huntsman/nn/tasks/Cargo.toml create mode 100644 examples/huntsman/nn/tasks/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index bede9c7df..8d5b20d95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,6 +853,15 @@ dependencies = [ name = "huntsman-nn-core" version = "0.1.0" +[[package]] +name = "huntsman-nn-tasks" +version = "0.1.0" +dependencies = [ + "huntsman-nn-core", + "serde", + "spider-tdl", +] + [[package]] name = "hyper" version = "1.10.1" diff --git a/Cargo.toml b/Cargo.toml index ddd377fcd..288f2a808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "examples/huntsman/complex/tasks", "examples/huntsman/complex/types", "examples/huntsman/nn/core", + "examples/huntsman/nn/tasks", "tests/huntsman/em-runtime", "tests/huntsman/integration-test-tasks", "tests/huntsman/task-executor", diff --git a/examples/huntsman/nn/tasks/Cargo.toml b/examples/huntsman/nn/tasks/Cargo.toml new file mode 100644 index 000000000..d1d9388d9 --- /dev/null +++ b/examples/huntsman/nn/tasks/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "huntsman-nn-tasks" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] +name = "huntsman_nn" +path = "src/lib.rs" + +[dependencies] +huntsman-nn-core = { path = "../core" } +serde = { version = "1.0.228", features = ["derive"] } +spider-tdl = { + path = "../../../../components/spider-tdl", + features = ["derive"] +} \ No newline at end of file diff --git a/examples/huntsman/nn/tasks/src/lib.rs b/examples/huntsman/nn/tasks/src/lib.rs new file mode 100644 index 000000000..9dc6e6d99 --- /dev/null +++ b/examples/huntsman/nn/tasks/src/lib.rs @@ -0,0 +1,121 @@ +//! Reference TDL package: dense-neuron computation. + +#![allow(clippy::too_many_arguments)] + +mod task_decl { + use spider_tdl::{TaskContext, TdlError, r#std::double, task}; + + #[task(name = "nn::dense_relu")] + pub fn dense_relu( + _ctx: TaskContext, + x0: double, + x1: double, + x2: double, + x3: double, + x4: double, + x5: double, + x6: double, + x7: double, + x8: double, + x9: double, + x10: double, + x11: double, + x12: double, + x13: double, + x14: double, + x15: double, + x16: double, + x17: double, + x18: double, + x19: double, + x20: double, + x21: double, + x22: double, + x23: double, + x24: double, + ) -> Result { + Ok(huntsman_nn_core::dense_relu(&[ + x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, + x19, x20, x21, x22, x23, x24, + ])) + } + + #[task(name = "nn::dense_sigmoid")] + pub fn dense_sigmoid( + _ctx: TaskContext, + x0: double, + x1: double, + x2: double, + x3: double, + x4: double, + x5: double, + x6: double, + x7: double, + x8: double, + x9: double, + x10: double, + x11: double, + x12: double, + x13: double, + x14: double, + x15: double, + x16: double, + x17: double, + x18: double, + x19: double, + x20: double, + x21: double, + x22: double, + x23: double, + x24: double, + ) -> Result { + Ok(huntsman_nn_core::dense_sigmoid(&[ + x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, + x19, x20, x21, x22, x23, x24, + ])) + } + + #[task(name = "nn::dense_identity")] + pub fn dense_identity( + _ctx: TaskContext, + x0: double, + x1: double, + x2: double, + x3: double, + x4: double, + x5: double, + x6: double, + x7: double, + x8: double, + x9: double, + x10: double, + x11: double, + x12: double, + x13: double, + x14: double, + x15: double, + x16: double, + x17: double, + x18: double, + x19: double, + x20: double, + x21: double, + x22: double, + x23: double, + x24: double, + ) -> Result { + Ok(huntsman_nn_core::dense_identity(&[ + x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, + x19, x20, x21, x22, x23, x24, + ])) + } +} + +spider_tdl::register_tdl_package! { + package_name: "nn", + tasks: [ + task_decl::dense_relu, + task_decl::dense_sigmoid, + task_decl::dense_identity, + ], +} From f2d26ff3611de9cdc7ced96bed8002ed04ed62b3 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Mon, 6 Jul 2026 11:49:01 -0400 Subject: [PATCH 03/14] Fix toml lint --- examples/huntsman/nn/core/Cargo.toml | 2 +- examples/huntsman/nn/tasks/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/huntsman/nn/core/Cargo.toml b/examples/huntsman/nn/core/Cargo.toml index f3e4c6791..790f5fa46 100644 --- a/examples/huntsman/nn/core/Cargo.toml +++ b/examples/huntsman/nn/core/Cargo.toml @@ -6,4 +6,4 @@ publish = false [lib] name = "huntsman_nn_core" -path = "src/lib.rs" \ No newline at end of file +path = "src/lib.rs" diff --git a/examples/huntsman/nn/tasks/Cargo.toml b/examples/huntsman/nn/tasks/Cargo.toml index d1d9388d9..b05f8ed7d 100644 --- a/examples/huntsman/nn/tasks/Cargo.toml +++ b/examples/huntsman/nn/tasks/Cargo.toml @@ -15,4 +15,4 @@ serde = { version = "1.0.228", features = ["derive"] } spider-tdl = { path = "../../../../components/spider-tdl", features = ["derive"] -} \ No newline at end of file +} From 8588bfa392f190cf718508648a8a85e0536696af Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 00:19:31 -0400 Subject: [PATCH 04/14] Add client --- Cargo.lock | 15 + Cargo.toml | 1 + examples/huntsman/nn/client/Cargo.toml | 20 ++ examples/huntsman/nn/client/src/main.rs | 398 ++++++++++++++++++++++++ 4 files changed, 434 insertions(+) create mode 100644 examples/huntsman/nn/client/Cargo.toml create mode 100644 examples/huntsman/nn/client/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 8d5b20d95..f949f5942 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,6 +849,21 @@ dependencies = [ "spider-tdl", ] +[[package]] +name = "huntsman-nn-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "huntsman-nn-core", + "rand 0.9.4", + "rmp-serde", + "spider-client", + "spider-core", + "tokio", + "tonic", +] + [[package]] name = "huntsman-nn-core" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 288f2a808..675c2123c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "components/spider-utils", "examples/huntsman/complex/tasks", "examples/huntsman/complex/types", + "examples/huntsman/nn/client", "examples/huntsman/nn/core", "examples/huntsman/nn/tasks", "tests/huntsman/em-runtime", diff --git a/examples/huntsman/nn/client/Cargo.toml b/examples/huntsman/nn/client/Cargo.toml new file mode 100644 index 000000000..95691c581 --- /dev/null +++ b/examples/huntsman/nn/client/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "huntsman-nn-client" +version = "0.1.0" +edition = "2024" +publish = false + +[[bin]] +name = "huntsman-nn-client" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.98" +clap = { version = "4.6.1", features = ["derive"] } +huntsman-nn-core = { path = "../core" } +rand = "0.9.1" +rmp-serde = "1.3.1" +spider-client = { path = "../../../../components/spider-client" } +spider-core = { path = "../../../../components/spider-core" } +tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread"] } +tonic = "0.14.6" diff --git a/examples/huntsman/nn/client/src/main.rs b/examples/huntsman/nn/client/src/main.rs new file mode 100644 index 000000000..3ff5b041e --- /dev/null +++ b/examples/huntsman/nn/client/src/main.rs @@ -0,0 +1,398 @@ +//! Spider client that builds a randomly-wired, neural-network-shaped `nn::dense_*` task graph and +//! runs it on a live Spider instance. + +use std::{num::NonZeroUsize, time::Duration}; + +use anyhow::{Context, anyhow}; +use clap::Parser; +use huntsman_nn_core::NUM_INPUTS; +use rand::{ + CryptoRng, + Rng, + SeedableRng, + rngs::StdRng, + seq::{IndexedRandom, index}, +}; +use spider_client::SpiderClient; +use spider_core::{ + job::JobState, + task::{ + DataTypeDescriptor, + TaskDescriptor, + TaskGraph, + TaskIndex, + TaskInputOutputIndex, + TdlContext, + ValueTypeDescriptor, + }, + types::{id::JobId, io::TaskInput}, +}; +use tonic::transport::Endpoint; + +/// Name of the TDL package supplying the `nn::dense_*` tasks. +const PACKAGE: &str = "nn"; + +/// An activation function pair. +#[derive(Clone, Copy)] +struct Activation { + /// The `nn::dense_*` task function name. + task_func: &'static str, + /// The core `dense_*` fn. + evaluate: fn(&[f64; NUM_INPUTS]) -> f64, +} + +/// The three `nn::dense_*` activations. +const ACTIVATIONS: &[Activation] = &[ + Activation { + task_func: "nn::dense_relu", + evaluate: huntsman_nn_core::dense_relu, + }, + Activation { + task_func: "nn::dense_sigmoid", + evaluate: huntsman_nn_core::dense_sigmoid, + }, + Activation { + task_func: "nn::dense_identity", + evaluate: huntsman_nn_core::dense_identity, + }, +]; + +/// Command-line arguments for the client. +#[derive(Debug, Parser)] +#[command( + about = "Build a randomly-wired nn::dense_* task graph and run it on the Spider instance." +)] +struct Cli { + /// Spider storage gRPC endpoint to connect to. + #[arg(long, value_name = "URL", default_value = "http://127.0.0.1:50051")] + endpoint: String, + + /// Number of layers in task graph. + #[arg(long, default_value_t = 10)] + level: usize, + + /// Number of neurons per layer. Must be at least the neuron fan-in (25). + #[arg(long, default_value_t = 1000)] + width: usize, + + /// Seed for the random task-graph topology. + #[arg(long, value_name = "UINT")] + seed: Option, + + /// gRPC connection pool size. + #[arg(long, default_value_t = 4)] + grpc_pool_size: usize, +} + +/// Topology of one layer of the graph. +struct Layer { + /// Activation applied to every neuron in this layer. + activation: Activation, + /// Wiring of previous layer's output to current layer's input. + /// `None` for layer 0, whose inputs come from the graph inputs. + /// For other layers, `wiring[i][k]` is the `k`-th previous-layer output index feeding neuron + /// `i`. + wiring: Option>>, +} + +/// Topology of the task graph, in layer order. +type Topology = Vec; + +/// # Returns +/// +/// The topology of a random graph. +/// +/// For each layer, draw a random activation from [`ACTIVATIONS`] and, for every +/// neuron, 25 distinct previous-layer output indices to feed it expect for layer 0. +/// +/// # Panics +/// +/// Panics if [`ACTIVATIONS`] is empty. +fn generate_topology(level: usize, width: usize, rng: &mut StdRng) -> Topology { + let mut topology = Vec::with_capacity(level); + for layer in 0..level { + let activation = *ACTIVATIONS.choose(rng).expect("`ACTIVATIONS` is non-empty"); + let wiring = if layer == 0 { + None + } else { + Some( + (0..width) + .map(|_| index::sample(rng, width, NUM_INPUTS).into_iter().collect()) + .collect(), + ) + }; + topology.push(Layer { activation, wiring }); + } + topology +} + +/// # Returns +/// +/// A task graph following the `topology`. +/// +/// # Errors +/// +/// Forwards [`TaskGraph::new`]'s return values on failure. +/// Forwards [`TaskGraph::insert_task`]'s return values on failure. +fn build_graph(width: usize, topology: &Topology) -> anyhow::Result { + let float64 = DataTypeDescriptor::Value(ValueTypeDescriptor::float64()); + let mut graph = TaskGraph::new(None, None)?; + let mut prev_layer: Vec = Vec::with_capacity(width); + + // Layer 0 + for _ in 0..width { + let task_idx = graph.insert_task(TaskDescriptor { + tdl_context: TdlContext { + package: PACKAGE.to_owned(), + task_func: topology[0].activation.task_func.to_owned(), + }, + execution_policy: None, + inputs: vec![float64.clone(); NUM_INPUTS], + outputs: vec![float64.clone()], + input_sources: None, + })?; + prev_layer.push(task_idx); + } + + for layer in &topology[1..] { + let mut curr_layer = Vec::with_capacity(width); + for task_input_sources in layer.wiring.as_ref().expect("Inner layer wiring is set") { + let input_sources: Vec = task_input_sources + .iter() + .map(|&src| TaskInputOutputIndex { + task_idx: prev_layer[src], + position: 0, + }) + .collect(); + let task_idx = graph.insert_task(TaskDescriptor { + tdl_context: TdlContext { + package: PACKAGE.to_owned(), + task_func: layer.activation.task_func.to_owned(), + }, + execution_policy: None, + inputs: vec![float64.clone(); NUM_INPUTS], + outputs: vec![float64.clone()], + input_sources: Some(input_sources), + })?; + curr_layer.push(task_idx); + } + prev_layer = curr_layer; + } + + Ok(graph) +} + +/// # Returns +/// +/// Randomly-generated task graph inputs. +/// +/// # Panics +/// +/// Panics if `width * 25` overflows `usize`. +fn generate_graph_inputs(width: usize, rng: &mut StdRng) -> Vec { + let count = width + .checked_mul(NUM_INPUTS) + .expect("number of graph inputs overflow"); + (0..count).map(|_| rng.random::()).collect() +} + +/// Executes the neural network. +/// +/// # Returns +/// +/// The neural network outputs. +fn simulate(width: usize, topology: &Topology, inputs: &[f64]) -> Vec { + let mut layer_outputs: Vec = (0..width) + .map(|i| { + let start = i * NUM_INPUTS; + let mut neuron_inputs = [0.0_f64; NUM_INPUTS]; + neuron_inputs.copy_from_slice(&inputs[start..start + NUM_INPUTS]); + (topology[0].activation.evaluate)(&neuron_inputs) + }) + .collect(); + + for layer in &topology[1..] { + let wiring = layer.wiring.as_ref().expect("Inner layer wiring is set"); + layer_outputs = wiring + .iter() + .map(|sources| { + let neuron_inputs: [f64; NUM_INPUTS] = + std::array::from_fn(|k| layer_outputs[sources[k]]); + (layer.activation.evaluate)(&neuron_inputs) + }) + .collect(); + } + + layer_outputs +} + +/// # Returns +/// +/// The msgpack-encoded graph inputs on success. +/// +/// # Errors +/// +/// Forwards [`rmp_serde::to_vec`]'s return values on failure. +fn encode_graph_inputs(graph_inputs: &[f64]) -> anyhow::Result> { + graph_inputs + .iter() + .map(|value| { + Ok::(TaskInput::ValuePayload(rmp_serde::to_vec(value)?)) + }) + .collect() +} + +/// # Returns +/// +/// Randomly-generated 32-bytes password. +fn generate_password(rng: &mut (impl Rng + CryptoRng)) -> Vec { + let mut bytes = [0u8; 32]; + rng.fill(&mut bytes[..]); + bytes.to_vec() +} + +/// Periodically polls the job state until it reaches a terminal state. +/// +/// # Returns +/// +/// The terminal [`JobState`] on success. +/// +/// # Errors +/// +/// Forwards [`SpiderClient::get_job_state`]'s return values on failure. +async fn poll_until_terminal(client: &SpiderClient, job_id: JobId) -> anyhow::Result { + loop { + let state = client + .get_job_state(job_id) + .await + .context("get_job_state")?; + if state.is_terminal() { + return Ok(state); + } + tokio::time::sleep(Duration::from_millis(500)).await; + } +} + +/// # Returns +/// +/// The decoded job outputs. +/// +/// # Errors +/// +/// Forwards [`SpiderClient::get_job_outputs`]'s return values on failure. +/// Forwards [`rmp_serde::from_slice`]'s return values on failure. +async fn fetch_outputs(client: &SpiderClient, job_id: JobId) -> anyhow::Result> { + let outputs = client + .get_job_outputs(job_id) + .await + .context("get_job_outputs")?; + outputs + .iter() + .enumerate() + .map(|(i, output)| { + rmp_serde::from_slice(output).with_context(|| format!("failed to decode output {i}")) + }) + .collect() +} + +/// Checks each output against the expected value, and print each mismatch. +/// +/// # Returns +/// +/// `Ok(())` on success and all outputs match. +/// +/// # Errors +/// +/// Returns an error if: +/// +/// * The output count differs from the expected count. +/// * One or more outputs mismatch the expected value within tolerance. +fn verify_outputs(outputs: &[f64], expected: &[f64]) -> anyhow::Result<()> { + anyhow::ensure!( + outputs.len() == expected.len(), + "Expected {} graph outputs, got {}", + expected.len(), + outputs.len() + ); + let mut mismatches = 0; + for (i, (&got, &exp)) in outputs.iter().zip(expected.iter()).enumerate() { + let diff = (got - exp).abs(); + let tol = 1.0e-9_f64 * (1.0 + exp.abs()); + if diff > tol { + mismatches += 1; + println!("output[{i}] got={got} expected={exp}"); + } + } + if mismatches == 0 { + return Ok(()); + } + Err(anyhow!("{mismatches}/{} wrong output", outputs.len())) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + if cli.level == 0 { + return Err(anyhow!("level must be >= 1")); + } + if cli.width < NUM_INPUTS { + return Err(anyhow!("width must be >= {NUM_INPUTS} (the neuron fan-in)")); + } + let pool_size = NonZeroUsize::new(cli.grpc_pool_size).context("grpc-pool-size must be >= 1")?; + + let endpoint: Endpoint = cli + .endpoint + .parse() + .with_context(|| format!("invalid endpoint {:?}", cli.endpoint))?; + let client = SpiderClient::connect(endpoint, pool_size).await?; + + let seed = cli.seed.unwrap_or_else(rand::random::); + let mut rng = StdRng::seed_from_u64(seed); + + let topology = generate_topology(cli.level, cli.width, &mut rng); + + let graph_inputs = generate_graph_inputs(cli.width, &mut rng); + let expected = simulate(cli.width, &topology, &graph_inputs); + let graph = build_graph(cli.width, &topology)?; + let task_inputs = encode_graph_inputs(&graph_inputs)?; + + let mut entropy_rng = rand::rng(); + let resource_group_id = client + .add_resource_group( + format!("huntsman-nn-{:x}", entropy_rng.random::()), + generate_password(&mut entropy_rng), + ) + .await + .context("add_resource_group")?; + + let job_id = client + .submit_job(resource_group_id, &graph, task_inputs) + .await + .context("submit_job")?; + client.start_job(job_id).await.context("start_job")?; + + println!( + "Submitted layered nn job: level={}, width={}, tasks={}, seed={}, job_id={}", + cli.level, + cli.width, + cli.level * cli.width, + seed, + job_id.get() + ); + + let state = poll_until_terminal(&client, job_id).await?; + match state { + JobState::Succeeded => { + let outputs = fetch_outputs(&client, job_id).await?; + verify_outputs(&outputs, &expected) + } + JobState::Failed => { + let message = client + .get_job_error(job_id) + .await + .context("get_job_error")?; + Err(anyhow!("job failed: {message}")) + } + other => Err(anyhow!("job ended in unexpected state {other:?}")), + } +} From 776a5a668b083c0e2bb3858e12032f8082ccfb64 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 00:26:25 -0400 Subject: [PATCH 05/14] Fix docstring --- examples/huntsman/nn/client/src/main.rs | 8 ++++---- examples/huntsman/nn/core/src/lib.rs | 12 ------------ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/examples/huntsman/nn/client/src/main.rs b/examples/huntsman/nn/client/src/main.rs index 3ff5b041e..3c4c6395c 100644 --- a/examples/huntsman/nn/client/src/main.rs +++ b/examples/huntsman/nn/client/src/main.rs @@ -71,7 +71,7 @@ struct Cli { #[arg(long, default_value_t = 10)] level: usize, - /// Number of neurons per layer. Must be at least the neuron fan-in (25). + /// Number of neurons per layer. Must be at least the neuron fan-in ([`NUM_INPUTS`]). #[arg(long, default_value_t = 1000)] width: usize, @@ -102,8 +102,8 @@ type Topology = Vec; /// /// The topology of a random graph. /// -/// For each layer, draw a random activation from [`ACTIVATIONS`] and, for every -/// neuron, 25 distinct previous-layer output indices to feed it expect for layer 0. +/// For each layer, draw a random activation from [`ACTIVATIONS`] and, for each neuron, +/// [`NUM_INPUTS`] distinct previous-layer output indices to feed it expect for layer 0. /// /// # Panics /// @@ -188,7 +188,7 @@ fn build_graph(width: usize, topology: &Topology) -> anyhow::Result { /// /// # Panics /// -/// Panics if `width * 25` overflows `usize`. +/// Panics if `width * NUM_INPUTS` overflows `usize`. fn generate_graph_inputs(width: usize, rng: &mut StdRng) -> Vec { let count = width .checked_mul(NUM_INPUTS) diff --git a/examples/huntsman/nn/core/src/lib.rs b/examples/huntsman/nn/core/src/lib.rs index e8fcd760d..ed2056c6a 100644 --- a/examples/huntsman/nn/core/src/lib.rs +++ b/examples/huntsman/nn/core/src/lib.rs @@ -81,13 +81,9 @@ mod tests { #[test] fn test_sigmoid() { - // sigmoid(0) = 0.5; saturates toward 1 for large positive inputs, toward 0 for large - // negative inputs. For large positive x, exp(-x) underflows relative to 1.0 in `f64`, so - // sigmoid rounds to exactly 1.0. assert_approx_eq(sigmoid(0.0), 0.5); assert_approx_eq(sigmoid(100.0), 1.0); assert_approx_eq(sigmoid(-100.0), 0.0); - // Monotonic non-decreasing across the range. assert!(sigmoid(-1.0) < sigmoid(0.0)); assert!(sigmoid(0.0) < sigmoid(1.0)); } @@ -107,7 +103,6 @@ mod tests { #[test] fn test_weighted_sum_all_one_inputs() { - // sum_k WEIGHTS[k] = 0.01 * (1 + 2 + ... + 25) = 0.01 * 325 = 3.25; plus BIAS (0.5) = 3.75. let inputs = [1.0_f64; NUM_INPUTS]; assert_approx_eq(weighted_sum(&inputs), 3.75); } @@ -115,11 +110,9 @@ mod tests { #[test] fn test_dense_relu() { let zero = [0.0_f64; NUM_INPUTS]; - // zero inputs -> pre-activation = BIAS = 0.5 -> relu(0.5) = 0.5. assert_approx_eq(dense_relu(&zero), 0.5); let ones = [1.0_f64; NUM_INPUTS]; - // pre-activation = 3.75 -> relu(3.75) = 3.75. assert_approx_eq(dense_relu(&ones), 3.75); // Negative weighted sum (large negative inputs) clamps to 0 under relu. @@ -130,26 +123,21 @@ mod tests { #[test] fn test_dense_sigmoid() { let zero = [0.0_f64; NUM_INPUTS]; - // zero inputs -> pre-activation = BIAS = 0.5 -> sigmoid(0.5) = 1 / (1 + exp(-0.5)). assert_approx_eq(dense_sigmoid(&zero), sigmoid(BIAS)); let ones = [1.0_f64; NUM_INPUTS]; - // pre-activation = 3.75 -> sigmoid(3.75). assert_approx_eq(dense_sigmoid(&ones), sigmoid(3.75)); } #[test] fn test_dense_identity() { let zero = [0.0_f64; NUM_INPUTS]; - // zero inputs -> pre-activation = BIAS = 0.5 -> identity(0.5) = 0.5. assert_approx_eq(dense_identity(&zero), 0.5); let ones = [1.0_f64; NUM_INPUTS]; - // pre-activation = 3.75 -> identity(3.75) = 3.75. assert_approx_eq(dense_identity(&ones), 3.75); let neg = [-1000.0_f64; NUM_INPUTS]; - // identity preserves the (negative) pre-activation, unlike relu. assert_approx_eq(dense_identity(&neg), weighted_sum(&neg)); } } From 14720caec4257d929bf2e8015fbf0229dc8fc10a Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 00:34:52 -0400 Subject: [PATCH 06/14] Replace print with logging --- Cargo.lock | 2 ++ examples/huntsman/nn/client/Cargo.toml | 2 ++ examples/huntsman/nn/client/src/main.rs | 29 +++++++++++++++---------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f949f5942..5cb62f574 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -860,8 +860,10 @@ dependencies = [ "rmp-serde", "spider-client", "spider-core", + "spider-utils", "tokio", "tonic", + "tracing", ] [[package]] diff --git a/examples/huntsman/nn/client/Cargo.toml b/examples/huntsman/nn/client/Cargo.toml index 95691c581..5e4dc4dea 100644 --- a/examples/huntsman/nn/client/Cargo.toml +++ b/examples/huntsman/nn/client/Cargo.toml @@ -16,5 +16,7 @@ rand = "0.9.1" rmp-serde = "1.3.1" spider-client = { path = "../../../../components/spider-client" } spider-core = { path = "../../../../components/spider-core" } +spider-utils = { path = "../../../../components/spider-utils" } tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread"] } tonic = "0.14.6" +tracing = { version = "0.1.41", default-features = false, features = ["std"] } diff --git a/examples/huntsman/nn/client/src/main.rs b/examples/huntsman/nn/client/src/main.rs index 3c4c6395c..cf26a7036 100644 --- a/examples/huntsman/nn/client/src/main.rs +++ b/examples/huntsman/nn/client/src/main.rs @@ -27,6 +27,7 @@ use spider_core::{ }, types::{id::JobId, io::TaskInput}, }; +use spider_utils::logging::set_up_logging; use tonic::transport::Endpoint; /// Name of the TDL package supplying the `nn::dense_*` tasks. @@ -295,7 +296,7 @@ async fn fetch_outputs(client: &SpiderClient, job_id: JobId) -> anyhow::Result anyhow::Result<()> { let tol = 1.0e-9_f64 * (1.0 + exp.abs()); if diff > tol { mismatches += 1; - println!("output[{i}] got={got} expected={exp}"); + tracing::warn!( + output_index = i, + got, + expected = exp, + "Output mismatched the simulation." + ); } } if mismatches == 0 { + tracing::info!(count = outputs.len(), "All outputs match the simulation."); return Ok(()); } Err(anyhow!("{mismatches}/{} wrong output", outputs.len())) @@ -331,6 +338,7 @@ fn verify_outputs(outputs: &[f64], expected: &[f64]) -> anyhow::Result<()> { #[tokio::main] async fn main() -> anyhow::Result<()> { + let _log_guard = set_up_logging(); let cli = Cli::parse(); if cli.level == 0 { return Err(anyhow!("level must be >= 1")); @@ -365,25 +373,24 @@ async fn main() -> anyhow::Result<()> { .await .context("add_resource_group")?; + tracing::info!( + level = cli.level, + width = cli.width, + tasks = cli.level * cli.width, + "Submitting job.", + ); let job_id = client .submit_job(resource_group_id, &graph, task_inputs) .await .context("submit_job")?; + tracing::info!(job_id = job_id.get(), "Starting job."); client.start_job(job_id).await.context("start_job")?; - println!( - "Submitted layered nn job: level={}, width={}, tasks={}, seed={}, job_id={}", - cli.level, - cli.width, - cli.level * cli.width, - seed, - job_id.get() - ); - let state = poll_until_terminal(&client, job_id).await?; match state { JobState::Succeeded => { let outputs = fetch_outputs(&client, job_id).await?; + tracing::info!(count = outputs.len(), "Fetched job outputs."); verify_outputs(&outputs, &expected) } JobState::Failed => { From 83f6e936a0af2f74bfcfad3f95a51a63f918d46b Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 00:43:21 -0400 Subject: [PATCH 07/14] Fix library and bin name --- examples/huntsman/nn/client/Cargo.toml | 2 +- examples/huntsman/nn/tasks/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/huntsman/nn/client/Cargo.toml b/examples/huntsman/nn/client/Cargo.toml index 5e4dc4dea..3bf0c3762 100644 --- a/examples/huntsman/nn/client/Cargo.toml +++ b/examples/huntsman/nn/client/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" publish = false [[bin]] -name = "huntsman-nn-client" +name = "nn-client" path = "src/main.rs" [dependencies] diff --git a/examples/huntsman/nn/tasks/Cargo.toml b/examples/huntsman/nn/tasks/Cargo.toml index b05f8ed7d..29dee028d 100644 --- a/examples/huntsman/nn/tasks/Cargo.toml +++ b/examples/huntsman/nn/tasks/Cargo.toml @@ -6,7 +6,7 @@ publish = false [lib] crate-type = ["cdylib"] -name = "huntsman_nn" +name = "nn" path = "src/lib.rs" [dependencies] From 1e3cc5e506c6194f6bc32d15e9e28af1453c2ad6 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 10:52:22 -0400 Subject: [PATCH 08/14] polish --- examples/huntsman/nn/client/src/main.rs | 15 ++++++++------- examples/huntsman/nn/core/src/lib.rs | 25 +++++++++++++++++++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/examples/huntsman/nn/client/src/main.rs b/examples/huntsman/nn/client/src/main.rs index cf26a7036..cd29d4bca 100644 --- a/examples/huntsman/nn/client/src/main.rs +++ b/examples/huntsman/nn/client/src/main.rs @@ -101,10 +101,10 @@ type Topology = Vec; /// # Returns /// -/// The topology of a random graph. +/// The topology of a random graph. Each layer contains: /// -/// For each layer, draw a random activation from [`ACTIVATIONS`] and, for each neuron, -/// [`NUM_INPUTS`] distinct previous-layer output indices to feed it expect for layer 0. +/// * an activation drawn from [`ACTIVATIONS`], +/// * for each non-input neuron, a fan-in of previous-layer output indices, except for layer 0. /// /// # Panics /// @@ -157,7 +157,7 @@ fn build_graph(width: usize, topology: &Topology) -> anyhow::Result { for layer in &topology[1..] { let mut curr_layer = Vec::with_capacity(width); - for task_input_sources in layer.wiring.as_ref().expect("Inner layer wiring is set") { + for task_input_sources in layer.wiring.as_ref().expect("inner layer wiring is set") { let input_sources: Vec = task_input_sources .iter() .map(|&src| TaskInputOutputIndex { @@ -213,7 +213,7 @@ fn simulate(width: usize, topology: &Topology, inputs: &[f64]) -> Vec { .collect(); for layer in &topology[1..] { - let wiring = layer.wiring.as_ref().expect("Inner layer wiring is set"); + let wiring = layer.wiring.as_ref().expect("inner layer wiring is set"); layer_outputs = wiring .iter() .map(|sources| { @@ -318,8 +318,8 @@ fn verify_outputs(outputs: &[f64], expected: &[f64]) -> anyhow::Result<()> { let mut mismatches = 0; for (i, (&got, &exp)) in outputs.iter().zip(expected.iter()).enumerate() { let diff = (got - exp).abs(); - let tol = 1.0e-9_f64 * (1.0 + exp.abs()); - if diff > tol { + let tol = 1.0e-12_f64 * (1.0 + exp.abs()); + if !got.is_finite() || !exp.is_finite() || diff > tol { mismatches += 1; tracing::warn!( output_index = i, @@ -355,6 +355,7 @@ async fn main() -> anyhow::Result<()> { let client = SpiderClient::connect(endpoint, pool_size).await?; let seed = cli.seed.unwrap_or_else(rand::random::); + tracing::info!(seed, "Seeded the topology RNG."); let mut rng = StdRng::seed_from_u64(seed); let topology = generate_topology(cli.level, cli.width, &mut rng); diff --git a/examples/huntsman/nn/core/src/lib.rs b/examples/huntsman/nn/core/src/lib.rs index ed2056c6a..ebc1a336d 100644 --- a/examples/huntsman/nn/core/src/lib.rs +++ b/examples/huntsman/nn/core/src/lib.rs @@ -16,40 +16,57 @@ pub const WEIGHTS: [f64; NUM_INPUTS] = [ /// The fixed bias added to the weighted sum before the activation. pub const BIAS: f64 = 0.5; -/// Rectified-linear activation: `max(0.0, x)`. +/// # Returns +/// +/// The rectified-linear activation `max(0.0, x)`. #[must_use] pub const fn relu(x: f64) -> f64 { f64::max(0.0, x) } -/// Logistic sigmoid activation: `1.0 / (1.0 + exp(-x))`. +/// # Returns +/// +/// The logistic sigmoid activation `1.0 / (1.0 + exp(-x))`. #[must_use] pub fn sigmoid(x: f64) -> f64 { 1.0 / (1.0 + f64::exp(-x)) } -/// Identity activation: returns its argument unchanged. +/// # Returns +/// +/// The identity activation `x`. #[must_use] pub const fn identity(x: f64) -> f64 { x } +/// # Returns +/// +/// The rectified-linear activation of the weighted sum of `inputs` plus [`BIAS`]. #[must_use] pub fn dense_relu(inputs: &[f64; NUM_INPUTS]) -> f64 { relu(weighted_sum(inputs)) } +/// # Returns +/// +/// The logistic sigmoid of the weighted sum of `inputs` plus [`BIAS`]. #[must_use] pub fn dense_sigmoid(inputs: &[f64; NUM_INPUTS]) -> f64 { sigmoid(weighted_sum(inputs)) } +/// # Returns +/// +/// The weighted sum of `inputs` plus [`BIAS`], unchanged by the activation. #[must_use] pub fn dense_identity(inputs: &[f64; NUM_INPUTS]) -> f64 { identity(weighted_sum(inputs)) } -/// Computes the pre-activation `sum(WEIGHTS[k] * inputs[k])+ BIAS`. +/// # Returns +/// +/// The weighted sum `sum(WEIGHTS[k] * inputs[k]) + BIAS`. fn weighted_sum(inputs: &[f64; NUM_INPUTS]) -> f64 { let mut acc = BIAS; for (w, x) in WEIGHTS.iter().zip(inputs.iter()) { From e723a62353678c2bcf878b63b6d2f32a1c838ae9 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 12:40:55 -0400 Subject: [PATCH 09/14] Fix wiring --- examples/huntsman/nn/client/src/main.rs | 52 ++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/examples/huntsman/nn/client/src/main.rs b/examples/huntsman/nn/client/src/main.rs index cd29d4bca..77722180a 100644 --- a/examples/huntsman/nn/client/src/main.rs +++ b/examples/huntsman/nn/client/src/main.rs @@ -90,9 +90,9 @@ struct Layer { /// Activation applied to every neuron in this layer. activation: Activation, /// Wiring of previous layer's output to current layer's input. - /// `None` for layer 0, whose inputs come from the graph inputs. - /// For other layers, `wiring[i][k]` is the `k`-th previous-layer output index feeding neuron - /// `i`. + /// * [`None`] for layer 0, whose inputs come from the graph inputs. + /// * For other layers, `wiring[i][k]` is the `k`-th previous-layer output index feeding neuron + /// `i`. wiring: Option>>, } @@ -103,8 +103,11 @@ type Topology = Vec; /// /// The topology of a random graph. Each layer contains: /// -/// * an activation drawn from [`ACTIVATIONS`], -/// * for each non-input neuron, a fan-in of previous-layer output indices, except for layer 0. +/// * An activation drawn from [`ACTIVATIONS`], +/// * An optional wiring: +/// * [`None`] for layer 0. +/// * Randomly drawn wiring for other layers, with guarantee that all previous layer's neuron has +/// at least one output wired to this layer, so it won't become job output. /// /// # Panics /// @@ -118,7 +121,17 @@ fn generate_topology(level: usize, width: usize, rng: &mut StdRng) -> Topology { } else { Some( (0..width) - .map(|_| index::sample(rng, width, NUM_INPUTS).into_iter().collect()) + .map(|i| { + // Force previous-layer output `i` into neuron `i`'s fan-in so every + // previous-layer task feeds at least one next-layer neuron; otherwise an + // unreferenced intermediate task would surface as a job output. + let mut sources: Vec = + index::sample(rng, width, NUM_INPUTS).into_iter().collect(); + if !sources.contains(&i) { + sources[rng.random_range(0..NUM_INPUTS)] = i; + } + sources + }) .collect(), ) }; @@ -401,6 +414,33 @@ async fn main() -> anyhow::Result<()> { .context("get_job_error")?; Err(anyhow!("job failed: {message}")) } + JobState::Cancelled => Err(anyhow!("job cancelled")), other => Err(anyhow!("job ended in unexpected state {other:?}")), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inner_layer_wiring_covers_previous_layer() { + let mut rng = StdRng::seed_from_u64(0); + let width = 50; + let topology = generate_topology(5, width, &mut rng); + for layer in &topology[1..] { + let wiring = layer.wiring.as_ref().expect("inner layer wiring is set"); + let mut covered = vec![false; width]; + for sources in wiring { + assert_eq!(sources.len(), NUM_INPUTS); + for &src in sources { + covered[src] = true; + } + } + assert!( + covered.iter().all(|&c| c), + "uncovered previous-layer output" + ); + } + } +} From 430c859981766f5607871587c46d480712406a71 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 13:27:12 -0400 Subject: [PATCH 10/14] Fix docstring and logging --- examples/huntsman/nn/client/Cargo.toml | 2 +- examples/huntsman/nn/client/src/main.rs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/huntsman/nn/client/Cargo.toml b/examples/huntsman/nn/client/Cargo.toml index 3bf0c3762..fe8f11029 100644 --- a/examples/huntsman/nn/client/Cargo.toml +++ b/examples/huntsman/nn/client/Cargo.toml @@ -17,6 +17,6 @@ rmp-serde = "1.3.1" spider-client = { path = "../../../../components/spider-client" } spider-core = { path = "../../../../components/spider-core" } spider-utils = { path = "../../../../components/spider-utils" } -tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread", "time"] } tonic = "0.14.6" tracing = { version = "0.1.41", default-features = false, features = ["std"] } diff --git a/examples/huntsman/nn/client/src/main.rs b/examples/huntsman/nn/client/src/main.rs index 77722180a..69accecaa 100644 --- a/examples/huntsman/nn/client/src/main.rs +++ b/examples/huntsman/nn/client/src/main.rs @@ -148,6 +148,10 @@ fn generate_topology(level: usize, width: usize, rng: &mut StdRng) -> Topology { /// /// Forwards [`TaskGraph::new`]'s return values on failure. /// Forwards [`TaskGraph::insert_task`]'s return values on failure. +/// +/// # Panics +/// +/// Panics if an inner layer's [`Layer::wiring`] is [`None`]. fn build_graph(width: usize, topology: &Topology) -> anyhow::Result { let float64 = DataTypeDescriptor::Value(ValueTypeDescriptor::float64()); let mut graph = TaskGraph::new(None, None)?; @@ -215,6 +219,10 @@ fn generate_graph_inputs(width: usize, rng: &mut StdRng) -> Vec { /// # Returns /// /// The neural network outputs. +/// +/// # Panics +/// +/// Panics if an inner layer's [`Layer::wiring`] is [`None`]. fn simulate(width: usize, topology: &Topology, inputs: &[f64]) -> Vec { let mut layer_outputs: Vec = (0..width) .map(|i| { @@ -324,7 +332,7 @@ async fn fetch_outputs(client: &SpiderClient, job_id: JobId) -> anyhow::Result anyhow::Result<()> { anyhow::ensure!( outputs.len() == expected.len(), - "Expected {} graph outputs, got {}", + "expected {} graph outputs, got {}", expected.len(), outputs.len() ); From 910371a95a1bfb9c484a2169c10264d8fac1c8a1 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 15:40:28 -0400 Subject: [PATCH 11/14] Fix style --- examples/huntsman/nn/client/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/huntsman/nn/client/src/main.rs b/examples/huntsman/nn/client/src/main.rs index 69accecaa..64602dad1 100644 --- a/examples/huntsman/nn/client/src/main.rs +++ b/examples/huntsman/nn/client/src/main.rs @@ -38,6 +38,7 @@ const PACKAGE: &str = "nn"; struct Activation { /// The `nn::dense_*` task function name. task_func: &'static str, + /// The core `dense_*` fn. evaluate: fn(&[f64; NUM_INPUTS]) -> f64, } @@ -89,6 +90,7 @@ struct Cli { struct Layer { /// Activation applied to every neuron in this layer. activation: Activation, + /// Wiring of previous layer's output to current layer's input. /// * [`None`] for layer 0, whose inputs come from the graph inputs. /// * For other layers, `wiring[i][k]` is the `k`-th previous-layer output index feeding neuron From 01385f36ed94a70280e1ca3e97396aa361d7dff3 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Tue, 7 Jul 2026 21:35:48 -0400 Subject: [PATCH 12/14] Remove client component --- Cargo.lock | 17 - Cargo.toml | 1 - examples/huntsman/nn/client/Cargo.toml | 22 -- examples/huntsman/nn/client/src/main.rs | 456 ------------------------ 4 files changed, 496 deletions(-) delete mode 100644 examples/huntsman/nn/client/Cargo.toml delete mode 100644 examples/huntsman/nn/client/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5cb62f574..8d5b20d95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,23 +849,6 @@ dependencies = [ "spider-tdl", ] -[[package]] -name = "huntsman-nn-client" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "huntsman-nn-core", - "rand 0.9.4", - "rmp-serde", - "spider-client", - "spider-core", - "spider-utils", - "tokio", - "tonic", - "tracing", -] - [[package]] name = "huntsman-nn-core" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 675c2123c..288f2a808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ members = [ "components/spider-utils", "examples/huntsman/complex/tasks", "examples/huntsman/complex/types", - "examples/huntsman/nn/client", "examples/huntsman/nn/core", "examples/huntsman/nn/tasks", "tests/huntsman/em-runtime", diff --git a/examples/huntsman/nn/client/Cargo.toml b/examples/huntsman/nn/client/Cargo.toml deleted file mode 100644 index fe8f11029..000000000 --- a/examples/huntsman/nn/client/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "huntsman-nn-client" -version = "0.1.0" -edition = "2024" -publish = false - -[[bin]] -name = "nn-client" -path = "src/main.rs" - -[dependencies] -anyhow = "1.0.98" -clap = { version = "4.6.1", features = ["derive"] } -huntsman-nn-core = { path = "../core" } -rand = "0.9.1" -rmp-serde = "1.3.1" -spider-client = { path = "../../../../components/spider-client" } -spider-core = { path = "../../../../components/spider-core" } -spider-utils = { path = "../../../../components/spider-utils" } -tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread", "time"] } -tonic = "0.14.6" -tracing = { version = "0.1.41", default-features = false, features = ["std"] } diff --git a/examples/huntsman/nn/client/src/main.rs b/examples/huntsman/nn/client/src/main.rs deleted file mode 100644 index 64602dad1..000000000 --- a/examples/huntsman/nn/client/src/main.rs +++ /dev/null @@ -1,456 +0,0 @@ -//! Spider client that builds a randomly-wired, neural-network-shaped `nn::dense_*` task graph and -//! runs it on a live Spider instance. - -use std::{num::NonZeroUsize, time::Duration}; - -use anyhow::{Context, anyhow}; -use clap::Parser; -use huntsman_nn_core::NUM_INPUTS; -use rand::{ - CryptoRng, - Rng, - SeedableRng, - rngs::StdRng, - seq::{IndexedRandom, index}, -}; -use spider_client::SpiderClient; -use spider_core::{ - job::JobState, - task::{ - DataTypeDescriptor, - TaskDescriptor, - TaskGraph, - TaskIndex, - TaskInputOutputIndex, - TdlContext, - ValueTypeDescriptor, - }, - types::{id::JobId, io::TaskInput}, -}; -use spider_utils::logging::set_up_logging; -use tonic::transport::Endpoint; - -/// Name of the TDL package supplying the `nn::dense_*` tasks. -const PACKAGE: &str = "nn"; - -/// An activation function pair. -#[derive(Clone, Copy)] -struct Activation { - /// The `nn::dense_*` task function name. - task_func: &'static str, - - /// The core `dense_*` fn. - evaluate: fn(&[f64; NUM_INPUTS]) -> f64, -} - -/// The three `nn::dense_*` activations. -const ACTIVATIONS: &[Activation] = &[ - Activation { - task_func: "nn::dense_relu", - evaluate: huntsman_nn_core::dense_relu, - }, - Activation { - task_func: "nn::dense_sigmoid", - evaluate: huntsman_nn_core::dense_sigmoid, - }, - Activation { - task_func: "nn::dense_identity", - evaluate: huntsman_nn_core::dense_identity, - }, -]; - -/// Command-line arguments for the client. -#[derive(Debug, Parser)] -#[command( - about = "Build a randomly-wired nn::dense_* task graph and run it on the Spider instance." -)] -struct Cli { - /// Spider storage gRPC endpoint to connect to. - #[arg(long, value_name = "URL", default_value = "http://127.0.0.1:50051")] - endpoint: String, - - /// Number of layers in task graph. - #[arg(long, default_value_t = 10)] - level: usize, - - /// Number of neurons per layer. Must be at least the neuron fan-in ([`NUM_INPUTS`]). - #[arg(long, default_value_t = 1000)] - width: usize, - - /// Seed for the random task-graph topology. - #[arg(long, value_name = "UINT")] - seed: Option, - - /// gRPC connection pool size. - #[arg(long, default_value_t = 4)] - grpc_pool_size: usize, -} - -/// Topology of one layer of the graph. -struct Layer { - /// Activation applied to every neuron in this layer. - activation: Activation, - - /// Wiring of previous layer's output to current layer's input. - /// * [`None`] for layer 0, whose inputs come from the graph inputs. - /// * For other layers, `wiring[i][k]` is the `k`-th previous-layer output index feeding neuron - /// `i`. - wiring: Option>>, -} - -/// Topology of the task graph, in layer order. -type Topology = Vec; - -/// # Returns -/// -/// The topology of a random graph. Each layer contains: -/// -/// * An activation drawn from [`ACTIVATIONS`], -/// * An optional wiring: -/// * [`None`] for layer 0. -/// * Randomly drawn wiring for other layers, with guarantee that all previous layer's neuron has -/// at least one output wired to this layer, so it won't become job output. -/// -/// # Panics -/// -/// Panics if [`ACTIVATIONS`] is empty. -fn generate_topology(level: usize, width: usize, rng: &mut StdRng) -> Topology { - let mut topology = Vec::with_capacity(level); - for layer in 0..level { - let activation = *ACTIVATIONS.choose(rng).expect("`ACTIVATIONS` is non-empty"); - let wiring = if layer == 0 { - None - } else { - Some( - (0..width) - .map(|i| { - // Force previous-layer output `i` into neuron `i`'s fan-in so every - // previous-layer task feeds at least one next-layer neuron; otherwise an - // unreferenced intermediate task would surface as a job output. - let mut sources: Vec = - index::sample(rng, width, NUM_INPUTS).into_iter().collect(); - if !sources.contains(&i) { - sources[rng.random_range(0..NUM_INPUTS)] = i; - } - sources - }) - .collect(), - ) - }; - topology.push(Layer { activation, wiring }); - } - topology -} - -/// # Returns -/// -/// A task graph following the `topology`. -/// -/// # Errors -/// -/// Forwards [`TaskGraph::new`]'s return values on failure. -/// Forwards [`TaskGraph::insert_task`]'s return values on failure. -/// -/// # Panics -/// -/// Panics if an inner layer's [`Layer::wiring`] is [`None`]. -fn build_graph(width: usize, topology: &Topology) -> anyhow::Result { - let float64 = DataTypeDescriptor::Value(ValueTypeDescriptor::float64()); - let mut graph = TaskGraph::new(None, None)?; - let mut prev_layer: Vec = Vec::with_capacity(width); - - // Layer 0 - for _ in 0..width { - let task_idx = graph.insert_task(TaskDescriptor { - tdl_context: TdlContext { - package: PACKAGE.to_owned(), - task_func: topology[0].activation.task_func.to_owned(), - }, - execution_policy: None, - inputs: vec![float64.clone(); NUM_INPUTS], - outputs: vec![float64.clone()], - input_sources: None, - })?; - prev_layer.push(task_idx); - } - - for layer in &topology[1..] { - let mut curr_layer = Vec::with_capacity(width); - for task_input_sources in layer.wiring.as_ref().expect("inner layer wiring is set") { - let input_sources: Vec = task_input_sources - .iter() - .map(|&src| TaskInputOutputIndex { - task_idx: prev_layer[src], - position: 0, - }) - .collect(); - let task_idx = graph.insert_task(TaskDescriptor { - tdl_context: TdlContext { - package: PACKAGE.to_owned(), - task_func: layer.activation.task_func.to_owned(), - }, - execution_policy: None, - inputs: vec![float64.clone(); NUM_INPUTS], - outputs: vec![float64.clone()], - input_sources: Some(input_sources), - })?; - curr_layer.push(task_idx); - } - prev_layer = curr_layer; - } - - Ok(graph) -} - -/// # Returns -/// -/// Randomly-generated task graph inputs. -/// -/// # Panics -/// -/// Panics if `width * NUM_INPUTS` overflows `usize`. -fn generate_graph_inputs(width: usize, rng: &mut StdRng) -> Vec { - let count = width - .checked_mul(NUM_INPUTS) - .expect("number of graph inputs overflow"); - (0..count).map(|_| rng.random::()).collect() -} - -/// Executes the neural network. -/// -/// # Returns -/// -/// The neural network outputs. -/// -/// # Panics -/// -/// Panics if an inner layer's [`Layer::wiring`] is [`None`]. -fn simulate(width: usize, topology: &Topology, inputs: &[f64]) -> Vec { - let mut layer_outputs: Vec = (0..width) - .map(|i| { - let start = i * NUM_INPUTS; - let mut neuron_inputs = [0.0_f64; NUM_INPUTS]; - neuron_inputs.copy_from_slice(&inputs[start..start + NUM_INPUTS]); - (topology[0].activation.evaluate)(&neuron_inputs) - }) - .collect(); - - for layer in &topology[1..] { - let wiring = layer.wiring.as_ref().expect("inner layer wiring is set"); - layer_outputs = wiring - .iter() - .map(|sources| { - let neuron_inputs: [f64; NUM_INPUTS] = - std::array::from_fn(|k| layer_outputs[sources[k]]); - (layer.activation.evaluate)(&neuron_inputs) - }) - .collect(); - } - - layer_outputs -} - -/// # Returns -/// -/// The msgpack-encoded graph inputs on success. -/// -/// # Errors -/// -/// Forwards [`rmp_serde::to_vec`]'s return values on failure. -fn encode_graph_inputs(graph_inputs: &[f64]) -> anyhow::Result> { - graph_inputs - .iter() - .map(|value| { - Ok::(TaskInput::ValuePayload(rmp_serde::to_vec(value)?)) - }) - .collect() -} - -/// # Returns -/// -/// Randomly-generated 32-bytes password. -fn generate_password(rng: &mut (impl Rng + CryptoRng)) -> Vec { - let mut bytes = [0u8; 32]; - rng.fill(&mut bytes[..]); - bytes.to_vec() -} - -/// Periodically polls the job state until it reaches a terminal state. -/// -/// # Returns -/// -/// The terminal [`JobState`] on success. -/// -/// # Errors -/// -/// Forwards [`SpiderClient::get_job_state`]'s return values on failure. -async fn poll_until_terminal(client: &SpiderClient, job_id: JobId) -> anyhow::Result { - loop { - let state = client - .get_job_state(job_id) - .await - .context("get_job_state")?; - if state.is_terminal() { - return Ok(state); - } - tokio::time::sleep(Duration::from_millis(500)).await; - } -} - -/// # Returns -/// -/// The decoded job outputs. -/// -/// # Errors -/// -/// Forwards [`SpiderClient::get_job_outputs`]'s return values on failure. -/// Forwards [`rmp_serde::from_slice`]'s return values on failure. -async fn fetch_outputs(client: &SpiderClient, job_id: JobId) -> anyhow::Result> { - let outputs = client - .get_job_outputs(job_id) - .await - .context("get_job_outputs")?; - outputs - .iter() - .enumerate() - .map(|(i, output)| { - rmp_serde::from_slice(output).with_context(|| format!("failed to decode output {i}")) - }) - .collect() -} - -/// Checks each output against the expected value, and logs each mismatch. -/// -/// # Returns -/// -/// `Ok(())` on success and all outputs match. -/// -/// # Errors -/// -/// Returns an error if: -/// -/// * The output count differs from the expected count. -/// * One or more outputs mismatch the expected value within tolerance. -fn verify_outputs(outputs: &[f64], expected: &[f64]) -> anyhow::Result<()> { - anyhow::ensure!( - outputs.len() == expected.len(), - "expected {} graph outputs, got {}", - expected.len(), - outputs.len() - ); - let mut mismatches = 0; - for (i, (&got, &exp)) in outputs.iter().zip(expected.iter()).enumerate() { - let diff = (got - exp).abs(); - let tol = 1.0e-12_f64 * (1.0 + exp.abs()); - if !got.is_finite() || !exp.is_finite() || diff > tol { - mismatches += 1; - tracing::warn!( - output_index = i, - got, - expected = exp, - "Output mismatched the simulation." - ); - } - } - if mismatches == 0 { - tracing::info!(count = outputs.len(), "All outputs match the simulation."); - return Ok(()); - } - Err(anyhow!("{mismatches}/{} wrong output", outputs.len())) -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let _log_guard = set_up_logging(); - let cli = Cli::parse(); - if cli.level == 0 { - return Err(anyhow!("level must be >= 1")); - } - if cli.width < NUM_INPUTS { - return Err(anyhow!("width must be >= {NUM_INPUTS} (the neuron fan-in)")); - } - let pool_size = NonZeroUsize::new(cli.grpc_pool_size).context("grpc-pool-size must be >= 1")?; - - let endpoint: Endpoint = cli - .endpoint - .parse() - .with_context(|| format!("invalid endpoint {:?}", cli.endpoint))?; - let client = SpiderClient::connect(endpoint, pool_size).await?; - - let seed = cli.seed.unwrap_or_else(rand::random::); - tracing::info!(seed, "Seeded the topology RNG."); - let mut rng = StdRng::seed_from_u64(seed); - - let topology = generate_topology(cli.level, cli.width, &mut rng); - - let graph_inputs = generate_graph_inputs(cli.width, &mut rng); - let expected = simulate(cli.width, &topology, &graph_inputs); - let graph = build_graph(cli.width, &topology)?; - let task_inputs = encode_graph_inputs(&graph_inputs)?; - - let mut entropy_rng = rand::rng(); - let resource_group_id = client - .add_resource_group( - format!("huntsman-nn-{:x}", entropy_rng.random::()), - generate_password(&mut entropy_rng), - ) - .await - .context("add_resource_group")?; - - tracing::info!( - level = cli.level, - width = cli.width, - tasks = cli.level * cli.width, - "Submitting job.", - ); - let job_id = client - .submit_job(resource_group_id, &graph, task_inputs) - .await - .context("submit_job")?; - tracing::info!(job_id = job_id.get(), "Starting job."); - client.start_job(job_id).await.context("start_job")?; - - let state = poll_until_terminal(&client, job_id).await?; - match state { - JobState::Succeeded => { - let outputs = fetch_outputs(&client, job_id).await?; - tracing::info!(count = outputs.len(), "Fetched job outputs."); - verify_outputs(&outputs, &expected) - } - JobState::Failed => { - let message = client - .get_job_error(job_id) - .await - .context("get_job_error")?; - Err(anyhow!("job failed: {message}")) - } - JobState::Cancelled => Err(anyhow!("job cancelled")), - other => Err(anyhow!("job ended in unexpected state {other:?}")), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn inner_layer_wiring_covers_previous_layer() { - let mut rng = StdRng::seed_from_u64(0); - let width = 50; - let topology = generate_topology(5, width, &mut rng); - for layer in &topology[1..] { - let wiring = layer.wiring.as_ref().expect("inner layer wiring is set"); - let mut covered = vec![false; width]; - for sources in wiring { - assert_eq!(sources.len(), NUM_INPUTS); - for &src in sources { - covered[src] = true; - } - } - assert!( - covered.iter().all(|&c| c), - "uncovered previous-layer output" - ); - } - } -} From 304e0d12fab8ee1149ee321c6b55fdeb8c2cfcf2 Mon Sep 17 00:00:00 2001 From: LinZhihao-723 Date: Wed, 8 Jul 2026 16:57:30 -0400 Subject: [PATCH 13/14] Minor fixes. --- examples/huntsman/nn/core/src/lib.rs | 14 +++++++------- examples/huntsman/nn/tasks/src/lib.rs | 11 +++++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/examples/huntsman/nn/core/src/lib.rs b/examples/huntsman/nn/core/src/lib.rs index ebc1a336d..efc7800f6 100644 --- a/examples/huntsman/nn/core/src/lib.rs +++ b/examples/huntsman/nn/core/src/lib.rs @@ -7,10 +7,10 @@ pub const NUM_INPUTS: usize = 25; /// The fixed per-input weights, one per input position. Deterministic values calculated as -/// (`WEIGHTS[k] = (k + 1) * 0.01`). +/// (`WEIGHTS[k] = (k + 1) * 0.01 * (-1)^k`), alternating in sign starting positive. pub const WEIGHTS: [f64; NUM_INPUTS] = [ - 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, - 0.17, 0.18, 0.19, 0.20, 0.21, 0.22, 0.23, 0.24, 0.25, + 0.01, -0.02, 0.03, -0.04, 0.05, -0.06, 0.07, -0.08, 0.09, -0.10, 0.11, -0.12, 0.13, -0.14, + 0.15, -0.16, 0.17, -0.18, 0.19, -0.20, 0.21, -0.22, 0.23, -0.24, 0.25, ]; /// The fixed bias added to the weighted sum before the activation. @@ -121,7 +121,7 @@ mod tests { #[test] fn test_weighted_sum_all_one_inputs() { let inputs = [1.0_f64; NUM_INPUTS]; - assert_approx_eq(weighted_sum(&inputs), 3.75); + assert_approx_eq(weighted_sum(&inputs), 0.63); } #[test] @@ -130,7 +130,7 @@ mod tests { assert_approx_eq(dense_relu(&zero), 0.5); let ones = [1.0_f64; NUM_INPUTS]; - assert_approx_eq(dense_relu(&ones), 3.75); + assert_approx_eq(dense_relu(&ones), 0.63); // Negative weighted sum (large negative inputs) clamps to 0 under relu. let neg = [-1000.0_f64; NUM_INPUTS]; @@ -143,7 +143,7 @@ mod tests { assert_approx_eq(dense_sigmoid(&zero), sigmoid(BIAS)); let ones = [1.0_f64; NUM_INPUTS]; - assert_approx_eq(dense_sigmoid(&ones), sigmoid(3.75)); + assert_approx_eq(dense_sigmoid(&ones), sigmoid(0.63)); } #[test] @@ -152,7 +152,7 @@ mod tests { assert_approx_eq(dense_identity(&zero), 0.5); let ones = [1.0_f64; NUM_INPUTS]; - assert_approx_eq(dense_identity(&ones), 3.75); + assert_approx_eq(dense_identity(&ones), 0.63); let neg = [-1000.0_f64; NUM_INPUTS]; assert_approx_eq(dense_identity(&neg), weighted_sum(&neg)); diff --git a/examples/huntsman/nn/tasks/src/lib.rs b/examples/huntsman/nn/tasks/src/lib.rs index 9dc6e6d99..d398dd67a 100644 --- a/examples/huntsman/nn/tasks/src/lib.rs +++ b/examples/huntsman/nn/tasks/src/lib.rs @@ -3,9 +3,12 @@ #![allow(clippy::too_many_arguments)] mod task_decl { - use spider_tdl::{TaskContext, TdlError, r#std::double, task}; + use spider_tdl::TaskContext; + use spider_tdl::TdlError; + use spider_tdl::r#std::double; + use spider_tdl::task; - #[task(name = "nn::dense_relu")] + #[task(name = "neuron::dense_relu")] pub fn dense_relu( _ctx: TaskContext, x0: double, @@ -40,7 +43,7 @@ mod task_decl { ])) } - #[task(name = "nn::dense_sigmoid")] + #[task(name = "neuron::dense_sigmoid")] pub fn dense_sigmoid( _ctx: TaskContext, x0: double, @@ -75,7 +78,7 @@ mod task_decl { ])) } - #[task(name = "nn::dense_identity")] + #[task(name = "neuron::dense_identity")] pub fn dense_identity( _ctx: TaskContext, x0: double, From f6bbbed5157d59863fea701085baf638052bee35 Mon Sep 17 00:00:00 2001 From: Sitao Wang Date: Wed, 8 Jul 2026 17:01:49 -0400 Subject: [PATCH 14/14] Fix lint --- examples/huntsman/nn/tasks/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/huntsman/nn/tasks/src/lib.rs b/examples/huntsman/nn/tasks/src/lib.rs index 9dc6e6d99..474b1e077 100644 --- a/examples/huntsman/nn/tasks/src/lib.rs +++ b/examples/huntsman/nn/tasks/src/lib.rs @@ -3,7 +3,10 @@ #![allow(clippy::too_many_arguments)] mod task_decl { - use spider_tdl::{TaskContext, TdlError, r#std::double, task}; + use spider_tdl::TaskContext; + use spider_tdl::TdlError; + use spider_tdl::r#std::double; + use spider_tdl::task; #[task(name = "nn::dense_relu")] pub fn dense_relu(