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())
}
#[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 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);
let loss = outputs[0].clone();
let mut updated = self.clone();
for (i, name) in wrt.iter().enumerate() {
let grad = &outputs[i + 1];
let shape = self.param_shape(name);
let mut data = self
.param_binding(name)
.unwrap_or_else(|| panic!("train_step: param {name:?} is not bound"))
.to_vec();
opt.step(name, &shape, &mut data, grad);
updated = updated.with_param(*name, data);
}
opt.end_iteration();
(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 = "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)
}
}