use rlx_ir::Graph;
use crate::{GraphScope, Tensor};
#[derive(Clone, Debug)]
pub struct Func {
graph: Graph,
params: Vec<(String, Vec<f32>)>,
}
impl Func {
pub fn new(name: impl Into<String>, build: impl FnOnce(&mut GraphScope) -> Tensor) -> Self {
Self::multi(name, |s| vec![build(s)])
}
pub fn multi(
name: impl Into<String>,
build: impl FnOnce(&mut GraphScope) -> Vec<Tensor>,
) -> Self {
let mut scope = GraphScope::new(name);
let outs = build(&mut scope);
scope.set_outputs(outs);
Self {
graph: scope.finish(),
params: Vec::new(),
}
}
pub fn from_graph(graph: Graph) -> Self {
Self {
graph,
params: Vec::new(),
}
}
pub fn with_param(mut self, name: impl Into<String>, data: impl Into<Vec<f32>>) -> Self {
let name = name.into();
let data = data.into();
if let Some(slot) = self.params.iter_mut().find(|(n, _)| *n == name) {
slot.1 = data;
} else {
self.params.push((name, data));
}
self
}
pub fn graph(&self) -> &Graph {
&self.graph
}
pub fn output_count(&self) -> usize {
self.graph.outputs.len()
}
pub fn param_binding(&self, name: &str) -> Option<&[f32]> {
self.params
.iter()
.find(|(n, _)| n == name)
.map(|(_, d)| d.as_slice())
}
pub fn param_names(&self) -> Vec<String> {
self.graph
.nodes()
.iter()
.filter_map(|n| match &n.op {
rlx_ir::Op::Param { name } => Some(name.clone()),
_ => None,
})
.collect()
}
pub fn param_shape_of(&self, name: &str) -> Option<Vec<usize>> {
self.graph.nodes().iter().find_map(|n| match &n.op {
rlx_ir::Op::Param { name: pn } if pn == name => Some(
n.shape
.dims()
.iter()
.map(|d| match d {
rlx_ir::Dim::Static(s) => *s,
rlx_ir::Dim::Dynamic(_) => 0,
})
.collect(),
),
_ => None,
})
}
pub fn init_params(mut self, mut init: impl FnMut(&str, &[usize]) -> Vec<f32>) -> Self {
let specs: Vec<(String, Vec<usize>)> = self
.param_names()
.into_iter()
.map(|n| {
let dims = self.param_shape_of(&n).unwrap_or_default();
(n, dims)
})
.collect();
for (name, dims) in specs {
let data = init(&name, &dims);
self = self.with_param(name, data);
}
self
}
pub fn init_randn(self, seed: u64, stddev: f32) -> Self {
fn u01(state: &mut u64) -> f64 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut x = *state;
x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
x ^= x >> 31;
(x >> 11) as f64 / (1u64 << 53) as f64
}
let mut state = seed ^ 0x9E37_79B9_7F4A_7C15;
self.init_params(move |_name, dims| {
let n: usize = dims.iter().map(|&d| d.max(1)).product();
(0..n)
.map(|_| {
let u1 = u01(&mut state).max(1e-12);
let u2 = u01(&mut state);
let g = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
(g as f32) * stddev
})
.collect()
})
}
#[cfg(feature = "autodiff")]
fn derive(&self, graph: Graph) -> Func {
Func {
graph,
params: self.params.clone(),
}
}
#[cfg(feature = "autodiff")]
fn node_ids_by_name(&self, names: &[&str]) -> Vec<rlx_ir::NodeId> {
names
.iter()
.map(|want| {
self.graph
.nodes()
.iter()
.find_map(|n| match &n.op {
rlx_ir::Op::Input { name } | rlx_ir::Op::Param { name } if name == want => {
Some(n.id)
}
_ => None,
})
.unwrap_or_else(|| panic!("Func: no input or param named {want:?}"))
})
.collect()
}
}
#[cfg(feature = "autodiff")]
impl Func {
pub fn grad(&self, wrt: &[&str]) -> Func {
let ids = self.node_ids_by_name(wrt);
let mut bwd = rlx_autodiff::grad(&self.graph, &ids);
crate::tensor::bake_unit_seed(&mut bwd);
self.derive(bwd)
}
pub fn value_and_grad(&self, wrt: &[&str]) -> Func {
let ids = self.node_ids_by_name(wrt);
let mut bwd = rlx_autodiff::grad_with_loss(&self.graph, &ids);
crate::tensor::bake_unit_seed(&mut bwd);
self.derive(bwd)
}
pub fn value_and_grad_all(&self) -> Func {
let names = self.param_names();
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
self.value_and_grad(&refs)
}
pub fn grad_all(&self) -> Func {
let names = self.param_names();
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
self.grad(&refs)
}
pub fn vmap(&self, batched: &[&str], batch_size: usize) -> Func {
self.derive(rlx_autodiff::vmap(&self.graph, batched, batch_size))
}
pub fn jvp(&self, tangent_for: &[&str]) -> Func {
let ids = self.node_ids_by_name(tangent_for);
self.derive(rlx_autodiff::jvp(&self.graph, &ids))
}
pub fn hvp(&self, wrt: &[&str]) -> Func {
let ids = self.node_ids_by_name(wrt);
self.derive(rlx_autodiff::hvp(&self.graph, &ids))
}
}
#[cfg(feature = "eval")]
impl Func {
pub fn run(&self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
self.run_on(rlx_runtime::fastest_device_for(&self.graph), inputs)
}
pub fn run_on(&self, device: rlx_runtime::Device, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
let compiled = crate::cache::compiled(&self.graph, device);
let mut c = compiled.borrow_mut();
for (name, data) in &self.params {
c.set_param(name, data);
}
c.run(inputs)
}
pub fn jit(&self) -> Jitted {
self.jit_on(rlx_runtime::fastest_device_for(&self.graph))
}
pub fn jit_on(&self, device: rlx_runtime::Device) -> Jitted {
Jitted {
compiled: crate::cache::compiled(&self.graph, device),
params: self.params.clone(),
}
}
}
#[cfg(feature = "optim")]
impl Func {
pub fn train_step_at(
&self,
opt: &mut dyn rlx_optim::Optimizer,
schedule: &crate::LrSchedule,
step: usize,
wrt: &[&str],
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
opt.set_lr(schedule.lr_at(step));
self.train_step(opt, wrt, inputs)
}
pub fn train_step(
&self,
opt: &mut dyn rlx_optim::Optimizer,
wrt: &[&str],
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
let outputs = self.value_and_grad(wrt).run(inputs);
self.apply_optimizer_step(opt, wrt, &outputs)
}
pub fn train_step_on(
&self,
device: rlx_runtime::Device,
opt: &mut dyn rlx_optim::Optimizer,
wrt: &[&str],
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
let outputs = self.value_and_grad(wrt).run_on(device, inputs);
self.apply_optimizer_step(opt, wrt, &outputs)
}
pub fn train_step_at_on(
&self,
device: rlx_runtime::Device,
opt: &mut dyn rlx_optim::Optimizer,
schedule: &crate::LrSchedule,
step: usize,
wrt: &[&str],
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
opt.set_lr(schedule.lr_at(step));
self.train_step_on(device, opt, wrt, inputs)
}
pub fn train_step_all(
&self,
opt: &mut dyn rlx_optim::Optimizer,
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
let names = self.param_names();
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
self.train_step(opt, &refs, inputs)
}
pub fn train_step_all_at(
&self,
opt: &mut dyn rlx_optim::Optimizer,
schedule: &crate::LrSchedule,
step: usize,
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
opt.set_lr(schedule.lr_at(step));
self.train_step_all(opt, inputs)
}
pub fn train_step_all_on(
&self,
device: rlx_runtime::Device,
opt: &mut dyn rlx_optim::Optimizer,
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
let names = self.param_names();
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
self.train_step_on(device, opt, &refs, inputs)
}
pub fn train_step_all_at_on(
&self,
device: rlx_runtime::Device,
opt: &mut dyn rlx_optim::Optimizer,
schedule: &crate::LrSchedule,
step: usize,
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
opt.set_lr(schedule.lr_at(step));
self.train_step_all_on(device, opt, inputs)
}
pub fn train_step_all_at_on_clipped(
&self,
device: rlx_runtime::Device,
opt: &mut dyn rlx_optim::Optimizer,
schedule: &crate::LrSchedule,
step: usize,
max_grad_norm: f32,
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
opt.set_lr(schedule.lr_at(step));
let names = self.param_names();
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
let mut outputs = self.value_and_grad(&refs).run_on(device, inputs);
let scale = grad_clip_scale(&outputs, max_grad_norm);
if scale < 1.0 {
for g in outputs.iter_mut().skip(1) {
for x in g.iter_mut() {
*x *= scale;
}
}
}
self.apply_optimizer_step(opt, &refs, &outputs)
}
pub fn train_step_all_at_on_qat(
&self,
device: rlx_runtime::Device,
opt: &mut dyn rlx_optim::Optimizer,
schedule: &crate::LrSchedule,
step: usize,
max_grad_norm: f32,
quant: impl Fn(&mut [f32]),
inputs: &[(&str, &[f32])],
) -> (Func, Vec<f32>) {
opt.set_lr(schedule.lr_at(step));
let names = self.param_names();
let refs: Vec<&str> = names.iter().map(String::as_str).collect();
let mut q = self.clone();
for name in &names {
let mut w = self
.param_binding(name)
.unwrap_or_else(|| panic!("qat: param {name:?} is not bound"))
.to_vec();
quant(&mut w);
q = q.with_param(name.clone(), w);
}
let mut outputs = q.value_and_grad(&refs).run_on(device, inputs);
let scale = grad_clip_scale(&outputs, max_grad_norm);
if scale < 1.0 {
for g in outputs.iter_mut().skip(1) {
for x in g.iter_mut() {
*x *= scale;
}
}
}
self.apply_optimizer_step(opt, &refs, &outputs)
}
fn apply_optimizer_step(
&self,
opt: &mut dyn rlx_optim::Optimizer,
wrt: &[&str],
outputs: &[Vec<f32>],
) -> (Func, Vec<f32>) {
let loss = outputs[0].clone();
let mut datas: Vec<Vec<f32>> = wrt
.iter()
.map(|name| {
self.param_binding(name)
.unwrap_or_else(|| panic!("train_step: param {name:?} is not bound"))
.to_vec()
})
.collect();
let shapes: Vec<Vec<usize>> = wrt.iter().map(|name| self.param_shape(name)).collect();
let mut items: Vec<rlx_optim::OptItem> = datas
.iter_mut()
.enumerate()
.map(|(i, data)| rlx_optim::OptItem {
name: wrt[i],
shape: &shapes[i],
param: data.as_mut_slice(),
grad: &outputs[i + 1],
})
.collect();
opt.step_batch(&mut items);
drop(items);
opt.end_iteration();
let mut updated = self.clone();
for (name, data) in wrt.iter().zip(datas) {
updated = updated.with_param(*name, data);
}
(updated, loss)
}
fn param_shape(&self, name: &str) -> Vec<usize> {
self.graph
.nodes()
.iter()
.find_map(|n| match &n.op {
rlx_ir::Op::Param { name: pn } if pn == name => Some(
n.shape
.dims()
.iter()
.map(|d| match d {
rlx_ir::Dim::Static(s) => *s,
rlx_ir::Dim::Dynamic(_) => 0,
})
.collect(),
),
_ => None,
})
.unwrap_or_else(|| panic!("train_step: no param named {name:?}"))
}
}
#[cfg(feature = "optim")]
fn grad_clip_scale(outputs: &[Vec<f32>], max_norm: f32) -> f32 {
if max_norm <= 0.0 {
return 1.0;
}
let mut sumsq = 0f64;
for g in outputs.iter().skip(1) {
for &x in g {
sumsq += (x as f64) * (x as f64);
}
}
let norm = sumsq.sqrt() as f32;
if norm.is_finite() && norm > max_norm {
max_norm / norm
} else {
1.0
}
}
#[cfg(feature = "eval")]
#[derive(Clone)]
pub struct Jitted {
compiled: std::rc::Rc<std::cell::RefCell<rlx_runtime::CompiledGraph>>,
params: Vec<(String, Vec<f32>)>,
}
#[cfg(feature = "eval")]
impl Jitted {
pub fn run(&self, inputs: &[(&str, &[f32])]) -> Vec<Vec<f32>> {
let mut c = self.compiled.borrow_mut();
for (name, data) in &self.params {
c.set_param(name, data);
}
c.run(inputs)
}
}