#![forbid(unsafe_code)]
use std::any::Any;
use std::path::Path;
use nuvec::tree::{LeafKind, LoadMode, Visit, VisitMut, Visitor, VisitorMut};
use nuvec::{Array, Category, Error, Result};
mod display;
pub mod layers;
pub mod losses;
pub mod optim;
mod rope_scaling;
pub mod sharded;
mod weights;
pub use display::{ModuleDisplay, Summary, SummaryRow};
pub use layers::{
Dropout, Embedding, LayerNorm, Linear, LoraBase, LoraLinear, QuantizedEmbedding,
QuantizedLinear, RmsNorm, Rope,
};
pub use nuvec::macros::Module;
pub use nuvec::{Param, Tree};
pub use rope_scaling::{Llama3, LongRope, RopeScaling, Yarn};
pub use weights::WeightSource;
#[cfg(doctest)]
mod site_doctests {
#[doc = include_str!("../../../site/content/docs/neural-networks.md")]
pub struct NeuralNetworks;
#[doc = include_str!("../../../site/content/docs/distributed.md")]
pub struct Distributed;
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub trait Module: Tree {
fn parameters(&self) -> Vec<(String, Array)> {
self.flatten()
}
fn trainable_parameters(&self) -> Vec<(String, Array)> {
let mut out = Vec::new();
visit_leaves(self, |p, a, _, trainable| {
if trainable {
out.push((p.to_owned(), a.clone()));
}
});
out
}
fn num_params(&self) -> usize {
let mut n = 0;
visit_leaves(self, |_, a, _, _| n += a.size());
n
}
fn num_trainable(&self) -> usize {
let mut n = 0;
visit_leaves(self, |_, a, _, trainable| {
if trainable {
n += a.size();
}
});
n
}
fn summary(&self) -> Summary {
Summary::of(self)
}
fn display(&self) -> ModuleDisplay<'_, Self> {
ModuleDisplay::new(self)
}
fn freeze(&mut self, filter: impl Fn(&str) -> bool) -> Result<usize> {
set_frozen(self, &filter, true)
}
fn unfreeze(&mut self, filter: impl Fn(&str) -> bool) -> Result<usize> {
set_frozen(self, &filter, false)
}
fn train(&mut self, on: bool) {
struct Train(bool);
impl VisitorMut for Train {
fn leaf(&mut self, _: &str, _: &mut Array, _: LeafKind, _: Option<&mut bool>) {}
fn training(&mut self, _: &str, flag: &mut bool) {
*flag = self.0;
}
}
self.visit_mut(&mut VisitMut::new(&mut Train(on)));
}
fn training(&self) -> bool {
struct Mode(Option<bool>);
impl Visitor for Mode {
fn leaf(&mut self, _: &str, _: &Array, _: LeafKind, _: bool) {}
fn mode(&mut self, _: &str, training: bool) {
self.0.get_or_insert(training);
}
}
let mut m = Mode(None);
self.visit(&mut Visit::new(&mut m));
m.0.unwrap_or(true)
}
fn load_weights(&mut self, src: impl WeightSource) -> Result<()> {
weights::load(self, src, LoadMode::Strict)
}
fn load_weights_partial(&mut self, src: impl WeightSource) -> Result<()> {
weights::load(self, src, LoadMode::Partial)
}
fn save_weights(&self, path: impl AsRef<Path>) -> Result<()> {
weights::save(self, path.as_ref(), false)
}
fn save_trainable(&self, path: impl AsRef<Path>) -> Result<()> {
weights::save(self, path.as_ref(), true)
}
fn map_modules<T: Tree>(
&mut self,
filter: impl Fn(&str) -> bool,
mut f: impl FnMut(&str, &mut T) -> Result<()>,
) -> Result<usize> {
struct Map<'a, T> {
filter: &'a dyn Fn(&str) -> bool,
f: &'a mut dyn FnMut(&str, &mut T) -> Result<()>,
count: usize,
error: Option<Error>,
}
impl<T: 'static> VisitorMut for Map<'_, T> {
fn leaf(&mut self, _: &str, _: &mut Array, _: LeafKind, _: Option<&mut bool>) {}
fn child(&mut self, path: &str, child: &mut dyn Any) -> bool {
if self.error.is_some() {
return false;
}
match child.downcast_mut::<T>() {
Some(m) if (self.filter)(path) => {
match (self.f)(path, m) {
Ok(()) => self.count += 1,
Err(e) => self.error = Some(e),
}
false
}
_ => true,
}
}
}
let mut map = Map {
filter: &filter,
f: &mut f,
count: 0,
error: None,
};
self.visit_mut(&mut VisitMut::new(&mut map));
match map.error {
Some(e) => Err(e),
None => Ok(map.count),
}
}
}
impl<T: Tree> Module for T {}
fn visit_leaves<T: Tree>(tree: &T, f: impl FnMut(&str, &Array, LeafKind, bool)) {
struct V<F>(F);
impl<F: FnMut(&str, &Array, LeafKind, bool)> Visitor for V<F> {
fn leaf(&mut self, path: &str, a: &Array, kind: LeafKind, trainable: bool) {
(self.0)(path, a, kind, trainable);
}
}
tree.visit(&mut Visit::new(&mut V(f)));
}
fn set_frozen<T: Tree>(tree: &mut T, filter: &dyn Fn(&str) -> bool, frozen: bool) -> Result<usize> {
if frozen {
let mut plain = Vec::new();
visit_leaves(tree, |p, _, kind, _| {
if kind == LeafKind::Array && filter(p) {
plain.push(p.to_owned());
}
});
if !plain.is_empty() {
return Err(Error::InvalidArgument(format!(
"freeze: plain arrays cannot be frozen (make them `Param`s): {}",
plain.join(", ")
)));
}
}
struct Set<'a> {
filter: &'a dyn Fn(&str) -> bool,
frozen: bool,
changed: usize,
}
impl VisitorMut for Set<'_> {
fn leaf(&mut self, path: &str, a: &mut Array, kind: LeafKind, flag: Option<&mut bool>) {
let Some(flag) = flag else { return };
if kind != LeafKind::Param || !(self.filter)(path) || *flag == self.frozen {
return;
}
if !self.frozen && !matches!(a.dtype().category(), Category::Float | Category::Complex)
{
return;
}
*flag = self.frozen;
self.changed += 1;
}
}
let mut set = Set {
filter,
frozen,
changed: 0,
};
tree.visit_mut(&mut VisitMut::new(&mut set));
Ok(set.changed)
}
#[track_caller]
pub fn relu(x: &Array) -> Array {
x.maximum(0.0)
}
#[track_caller]
pub fn silu(x: &Array) -> Array {
x.multiply(x.sigmoid())
}
#[track_caller]
pub fn gelu_approx(x: &Array) -> Array {
let cube = x.power(3.0);
let inner = x
.add(cube.multiply(0.044715))
.multiply((2.0 / std::f64::consts::PI).sqrt());
x.multiply(0.5).multiply(inner.tanh().add(1.0))
}
#[track_caller]
pub fn gelu_fast_approx(x: &Array) -> Array {
x.multiply(x.multiply(1.702).sigmoid())
}
type Compiled<T> = Box<dyn Fn(&T) -> Result<Array> + Send + Sync>;
fn weak(value: f32, like: &Array) -> Array {
Array::from(value).astype(like.dtype())
}
#[track_caller]
pub fn swiglu(gate: &Array, x: &Array) -> Array {
static F: std::sync::LazyLock<Compiled<(Array, Array)>> = std::sync::LazyLock::new(|| {
Box::new(nuvec::compile(|(g, x): &(Array, Array)| {
silu(g).multiply(x)
}))
});
F(&(gate.clone(), x.clone())).unwrap_or_else(|_| silu(gate).multiply(x))
}
#[track_caller]
pub fn swiglu_oai(gate: &Array, x: &Array, alpha: f32, limit: f32) -> Array {
fn graph(g: &Array, x: &Array, alpha: &Array, limit: &Array) -> Array {
let g = g.minimum(limit);
let x = x.maximum(limit.negative()).minimum(limit);
x.add(1.0)
.multiply(g.multiply(alpha.multiply(&g).sigmoid()))
}
static F: std::sync::LazyLock<Compiled<(Array, Array, Array, Array)>> =
std::sync::LazyLock::new(|| {
Box::new(nuvec::compile(
|(g, x, a, l): &(Array, Array, Array, Array)| graph(g, x, a, l),
))
});
let (a, l) = (weak(alpha, gate), weak(limit, gate));
F(&(gate.clone(), x.clone(), a.clone(), l.clone())).unwrap_or_else(|_| graph(gate, x, &a, &l))
}
#[track_caller]
pub fn geglu(gate: &Array, x: &Array) -> Array {
static F: std::sync::LazyLock<Compiled<(Array, Array)>> = std::sync::LazyLock::new(|| {
Box::new(nuvec::compile(|(g, x): &(Array, Array)| {
gelu_approx(g).multiply(x)
}))
});
F(&(gate.clone(), x.clone())).unwrap_or_else(|_| gelu_approx(gate).multiply(x))
}
#[track_caller]
pub fn logit_softcap(x: &Array, cap: f32) -> Array {
if !(cap > 0.0 && cap.is_finite()) {
return Array::failure(
"logit_softcap",
nuvec::OpErrorKind::InvalidArgument(format!(
"the cap must be positive and finite, got {cap}"
)),
);
}
fn graph(x: &Array, cap: &Array) -> Array {
x.divide(cap).tanh().multiply(cap)
}
static F: std::sync::LazyLock<Compiled<(Array, Array)>> = std::sync::LazyLock::new(|| {
Box::new(nuvec::compile(|(x, c): &(Array, Array)| graph(x, c)))
});
let c = weak(cap, x);
F(&(x.clone(), c.clone())).unwrap_or_else(|_| graph(x, &c))
}