use crate::common::{cast_positive, cast_scalar};
use crate::domain_optimizers::{clip_grad_norm, l2_norm, AdvancedOptimizer, OptimizerStateInfo};
use crate::error::{OptimError, Result};
use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct NLPOptimizer<T: Float + Debug + Send + Sync + 'static> {
base_lr: T,
current_lr: T,
layer_wise_decay: T,
num_layers: usize,
warmup_steps: usize,
total_steps: usize,
step_count: usize,
token_aware_scaling: bool,
gradient_accumulation_steps: usize,
accumulated_gradients: Option<Array1<T>>,
accumulation_count: usize,
max_grad_norm: T,
velocity: Option<Array1<T>>,
momentum: T,
grad_norm_ema: T,
ema_decay: T,
}
impl<T: Float + Debug + Send + Sync + 'static> NLPOptimizer<T> {
pub fn new(base_lr: T) -> Result<Self> {
Ok(Self {
base_lr,
current_lr: base_lr,
layer_wise_decay: T::one(),
num_layers: 1,
warmup_steps: 0,
total_steps: 0,
step_count: 0,
token_aware_scaling: false,
gradient_accumulation_steps: 1,
accumulated_gradients: None,
accumulation_count: 0,
max_grad_norm: T::one(),
velocity: None,
momentum: cast_scalar(0.9)?,
grad_norm_ema: T::zero(),
ema_decay: cast_scalar(0.999)?,
})
}
pub fn with_layer_wise_decay(mut self, decay: T, num_layers: usize) -> Self {
self.layer_wise_decay = decay;
self.num_layers = if num_layers == 0 { 1 } else { num_layers };
self
}
pub fn with_warmup_and_schedule(mut self, warmup: usize, total: usize) -> Self {
self.warmup_steps = warmup;
self.total_steps = total;
self
}
pub fn with_token_aware_scaling(mut self, enable: bool) -> Self {
self.token_aware_scaling = enable;
self
}
pub fn with_gradient_accumulation(mut self, steps: usize) -> Self {
self.gradient_accumulation_steps = if steps == 0 { 1 } else { steps };
self
}
pub fn with_max_grad_norm(mut self, norm: T) -> Self {
self.max_grad_norm = norm;
self
}
pub fn with_momentum(mut self, momentum: T) -> Self {
self.momentum = momentum;
self
}
pub fn get_layer_lr(&self, layer_idx: usize) -> T {
let exponent = if layer_idx < self.num_layers {
layer_idx
} else {
self.num_layers.saturating_sub(1)
};
let mut factor = T::one();
for _ in 0..exponent {
factor = factor * self.layer_wise_decay;
}
self.current_lr * factor
}
fn schedule_factor(&self) -> Result<T> {
if self.warmup_steps > 0 && self.step_count < self.warmup_steps {
let step_t: T = cast_scalar(self.step_count + 1)?;
let warmup_t: T = cast_positive(self.warmup_steps, "warmup_steps")?;
return Ok(step_t / warmup_t);
}
if self.total_steps > self.warmup_steps {
let progress_steps = self.step_count.saturating_sub(self.warmup_steps);
let decay_steps = self.total_steps - self.warmup_steps;
let progress_t: T = cast_scalar(progress_steps)?;
let decay_t: T = cast_positive(decay_steps, "decay steps")?;
let progress = progress_t / decay_t;
let progress = if progress > T::one() {
T::one()
} else {
progress
};
let pi: T = cast_scalar(std::f64::consts::PI)?;
let half: T = cast_scalar(0.5)?;
let cosine = (pi * progress).cos();
return Ok(half * (T::one() + cosine));
}
Ok(T::one())
}
fn apply_token_scaling(&self, gradients: &Array1<T>) -> Result<Array1<T>> {
if !self.token_aware_scaling {
return Ok(gradients.clone());
}
let len = gradients.len();
let mut scaled = gradients.clone();
let third = len / 3;
let embed_scale: T = cast_scalar(0.5)?;
let ffn_scale: T = cast_scalar(1.2)?;
for i in 0..third {
scaled[i] = scaled[i] * embed_scale;
}
for i in (len - third)..len {
scaled[i] = scaled[i] * ffn_scale;
}
Ok(scaled)
}
fn apply_layer_wise_decay(&self, gradients: &Array1<T>) -> Result<Array1<T>> {
let epsilon: T = cast_scalar(1e-12)?;
if (self.layer_wise_decay - T::one()).abs() < epsilon {
return Ok(gradients.clone());
}
let len = gradients.len();
let chunk = len / self.num_layers;
if chunk == 0 {
return Ok(gradients.clone());
}
let mut scaled = gradients.clone();
for layer in 0..self.num_layers {
let start = layer * chunk;
let end = if layer == self.num_layers - 1 {
len
} else {
start + chunk
};
let exponent = layer;
let mut factor = T::one();
for _ in 0..exponent {
factor = factor * self.layer_wise_decay;
}
for i in start..end {
scaled[i] = scaled[i] * factor;
}
}
Ok(scaled)
}
}
impl<T: Float + Debug + Send + Sync + 'static> AdvancedOptimizer<T> for NLPOptimizer<T> {
fn step(&mut self, params: &Array1<T>, gradients: &Array1<T>) -> Result<Array1<T>> {
if params.len() != gradients.len() {
return Err(OptimError::InvalidConfig(format!(
"Parameter length {} != gradient length {}",
params.len(),
gradients.len()
)));
}
if params.is_empty() {
return Err(OptimError::InsufficientData(
"Empty parameter array".to_string(),
));
}
let acc = match self.accumulated_gradients.take() {
Some(a) if a.len() == gradients.len() => a + gradients,
_ => gradients.clone(),
};
self.accumulation_count += 1;
if self.accumulation_count < self.gradient_accumulation_steps {
self.accumulated_gradients = Some(acc);
return Ok(params.clone());
}
let accum_steps_t: T = cast_positive(
self.gradient_accumulation_steps,
"gradient_accumulation_steps",
)?;
let grad = acc.mapv(|g| g / accum_steps_t);
self.accumulation_count = 0;
let grad = clip_grad_norm(&grad, self.max_grad_norm);
let grad = self.apply_token_scaling(&grad)?;
let grad = self.apply_layer_wise_decay(&grad)?;
let norm = l2_norm(&grad);
self.grad_norm_ema =
self.ema_decay * self.grad_norm_ema + (T::one() - self.ema_decay) * norm;
let schedule = self.schedule_factor()?;
let effective_lr = self.base_lr * schedule;
self.current_lr = effective_lr;
let velocity = match self.velocity.take() {
Some(v) if v.len() == params.len() => v,
_ => Array1::zeros(params.len()),
};
let velocity =
velocity.mapv(|v| v * self.momentum) + &grad.mapv(|g| g * (T::one() - self.momentum));
let updated = params - &velocity.mapv(|v| v * effective_lr);
self.velocity = Some(velocity);
self.step_count += 1;
Ok(updated)
}
fn get_learning_rate(&self) -> T {
self.current_lr
}
fn set_learning_rate(&mut self, lr: T) {
self.base_lr = lr;
self.current_lr = lr;
}
fn name(&self) -> &str {
"NLPOptimizer"
}
fn get_state(&self) -> OptimizerStateInfo<T> {
OptimizerStateInfo {
step_count: self.step_count,
current_lr: self.current_lr,
grad_norm_ema: self.grad_norm_ema,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
#[test]
fn test_nlp_optimizer_basic_step() {
let mut opt = NLPOptimizer::new(0.01_f64).expect("optimizer");
let params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let grads = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.1, 0.2, 0.3]);
let updated = opt.step(¶ms, &grads).expect("step should succeed");
for i in 0..6 {
assert!(updated[i] < params[i], "param {} should decrease", i);
}
assert_eq!(opt.step_count, 1);
}
#[test]
fn test_nlp_optimizer_warmup_and_cosine() {
let mut opt = NLPOptimizer::new(0.1_f64)
.expect("optimizer")
.with_warmup_and_schedule(10, 100);
let params = Array1::ones(6);
let grads = Array1::ones(6);
let mut lrs = Vec::new();
for _ in 0..15 {
let _ = opt.step(¶ms, &grads).expect("step ok");
lrs.push(opt.get_learning_rate());
}
assert!(lrs[4] > lrs[0], "LR should increase during warmup");
assert!(lrs[14] < lrs[9], "LR should decay after warmup via cosine");
}
#[test]
fn test_nlp_optimizer_layer_wise_decay() {
let opt = NLPOptimizer::new(0.01_f64)
.expect("optimizer")
.with_layer_wise_decay(0.8, 4);
let lr_top = opt.get_layer_lr(0);
let lr_bottom = opt.get_layer_lr(3);
assert!(
lr_top > lr_bottom,
"output (top) layer must have the highest LR, lr_top={lr_top}, lr_bottom={lr_bottom}"
);
approx::assert_abs_diff_eq!(lr_top, 0.01, epsilon = 1e-12);
approx::assert_abs_diff_eq!(lr_bottom, 0.01 * 0.8_f64.powi(3), epsilon = 1e-12);
assert!(opt.get_layer_lr(1) > opt.get_layer_lr(2));
}
#[test]
fn test_nlp_optimizer_gradient_accumulation() {
let mut opt = NLPOptimizer::new(0.01_f64)
.expect("optimizer")
.with_gradient_accumulation(4);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let grads = Array1::from_vec(vec![0.1, 0.2, 0.3]);
for _ in 0..3 {
let updated = opt.step(¶ms, &grads).expect("step ok");
assert_eq!(updated, params, "should not update during accumulation");
assert_eq!(
opt.step_count, 0,
"step_count should not advance during accumulation"
);
}
let updated = opt.step(¶ms, &grads).expect("step ok");
assert_ne!(updated, params, "should update after accumulation complete");
assert_eq!(opt.step_count, 1);
}
#[test]
fn test_nlp_optimizer_gradient_clipping() {
let mut opt = NLPOptimizer::new(0.01_f64)
.expect("optimizer")
.with_max_grad_norm(0.5)
.with_momentum(0.0);
let params = Array1::from_vec(vec![1.0, 1.0, 1.0]);
let grads = Array1::from_vec(vec![10.0, 10.0, 10.0]);
let updated = opt.step(¶ms, &grads).expect("step ok");
let diff_norm = l2_norm(&(¶ms - &updated));
assert!(
diff_norm < 0.02,
"gradient clipping should limit update, got {}",
diff_norm
);
}
#[test]
fn test_nlp_optimizer_token_aware_scaling() {
let mut opt_token = NLPOptimizer::new(0.01_f64)
.expect("optimizer")
.with_token_aware_scaling(true)
.with_max_grad_norm(100.0)
.with_momentum(0.0);
let mut opt_base = NLPOptimizer::new(0.01_f64)
.expect("optimizer")
.with_max_grad_norm(100.0)
.with_momentum(0.0);
let params = Array1::from_vec(vec![1.0; 9]);
let grads = Array1::from_vec(vec![1.0; 9]);
let u_token = opt_token.step(¶ms, &grads).expect("step ok");
let u_base = opt_base.step(¶ms, &grads).expect("step ok");
let delta_token_embed = (params[0] - u_token[0]).abs();
let delta_base_embed = (params[0] - u_base[0]).abs();
assert!(
delta_token_embed < delta_base_embed,
"token scaling should reduce embedding updates"
);
}
#[test]
fn test_nlp_optimizer_dimension_mismatch() {
let mut opt = NLPOptimizer::new(0.01_f64).expect("optimizer");
let params = Array1::from_vec(vec![1.0, 2.0]);
let grads = Array1::from_vec(vec![0.1]);
let result = opt.step(¶ms, &grads);
assert!(result.is_err());
}
#[test]
fn test_nlp_optimizer_state_info() {
let mut opt = NLPOptimizer::new(0.05_f64).expect("optimizer");
let params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let grads = Array1::from_vec(vec![0.1, 0.2, 0.3]);
let _ = opt.step(¶ms, &grads).expect("step ok");
let state = opt.get_state();
assert_eq!(state.step_count, 1);
assert!(state.grad_norm_ema > 0.0);
}
}