diff --git a/Cargo.lock b/Cargo.lock index 326b2b189..8d5b20d95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,6 +849,19 @@ dependencies = [ "spider-tdl", ] +[[package]] +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 c624e3e7e..288f2a808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ members = [ "components/spider-utils", "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/core/Cargo.toml b/examples/huntsman/nn/core/Cargo.toml new file mode 100644 index 000000000..790f5fa46 --- /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" diff --git a/examples/huntsman/nn/core/src/lib.rs b/examples/huntsman/nn/core/src/lib.rs new file mode 100644 index 000000000..efc7800f6 --- /dev/null +++ b/examples/huntsman/nn/core/src/lib.rs @@ -0,0 +1,160 @@ +//! 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 * (-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, +]; + +/// The fixed bias added to the weighted sum before the activation. +pub const BIAS: f64 = 0.5; + +/// # Returns +/// +/// The rectified-linear activation `max(0.0, x)`. +#[must_use] +pub const fn relu(x: f64) -> f64 { + f64::max(0.0, 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)) +} + +/// # 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)) +} + +/// # 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()) { + 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() { + assert_approx_eq(sigmoid(0.0), 0.5); + assert_approx_eq(sigmoid(100.0), 1.0); + assert_approx_eq(sigmoid(-100.0), 0.0); + 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() { + let inputs = [1.0_f64; NUM_INPUTS]; + assert_approx_eq(weighted_sum(&inputs), 0.63); + } + + #[test] + fn test_dense_relu() { + let zero = [0.0_f64; NUM_INPUTS]; + assert_approx_eq(dense_relu(&zero), 0.5); + + let ones = [1.0_f64; NUM_INPUTS]; + 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]; + assert_approx_eq(dense_relu(&neg), 0.0); + } + + #[test] + fn test_dense_sigmoid() { + let zero = [0.0_f64; NUM_INPUTS]; + assert_approx_eq(dense_sigmoid(&zero), sigmoid(BIAS)); + + let ones = [1.0_f64; NUM_INPUTS]; + assert_approx_eq(dense_sigmoid(&ones), sigmoid(0.63)); + } + + #[test] + fn test_dense_identity() { + let zero = [0.0_f64; NUM_INPUTS]; + assert_approx_eq(dense_identity(&zero), 0.5); + + let ones = [1.0_f64; NUM_INPUTS]; + 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/Cargo.toml b/examples/huntsman/nn/tasks/Cargo.toml new file mode 100644 index 000000000..29dee028d --- /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 = "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"] +} diff --git a/examples/huntsman/nn/tasks/src/lib.rs b/examples/huntsman/nn/tasks/src/lib.rs new file mode 100644 index 000000000..d398dd67a --- /dev/null +++ b/examples/huntsman/nn/tasks/src/lib.rs @@ -0,0 +1,124 @@ +//! Reference TDL package: dense-neuron computation. + +#![allow(clippy::too_many_arguments)] + +mod task_decl { + use spider_tdl::TaskContext; + use spider_tdl::TdlError; + use spider_tdl::r#std::double; + use spider_tdl::task; + + #[task(name = "neuron::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 = "neuron::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 = "neuron::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, + ], +}