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) -> Self {
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: T::from(0.9).expect("0.9 should convert"),
grad_norm_ema: T::zero(),
ema_decay: T::from(0.999).expect("0.999 should convert"),
}
}
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 {
self.num_layers - 1 - layer_idx
} else {
0
};
let mut factor = T::one();
for _ in 0..exponent {
factor = factor * self.layer_wise_decay;
}
self.current_lr * factor
}
fn schedule_factor(&self) -> T {
if self.warmup_steps > 0 && self.step_count < self.warmup_steps {
let step_t = T::from(self.step_count + 1).expect("step convert");
let warmup_t = T::from(self.warmup_steps).expect("warmup convert");
return 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::from(progress_steps).expect("progress convert")
/ T::from(decay_steps).expect("decay convert");
let progress = if progress > T::one() {
T::one()
} else {
progress
};
let pi = T::from(std::f64::consts::PI).expect("pi convert");
let half = T::from(0.5).expect("0.5 convert");
let cosine = (pi * progress).cos();
return half * (T::one() + cosine);
}
T::one()
}
fn apply_token_scaling(&self, gradients: &Array1<T>) -> Array1<T> {
if !self.token_aware_scaling {
return gradients.clone();
}
let len = gradients.len();
let mut scaled = gradients.clone();
let third = len / 3;
let embed_scale = T::from(0.5).expect("0.5 convert");
let ffn_scale = T::from(1.2).expect("1.2 convert");
for i in 0..third {
scaled[i] = scaled[i] * embed_scale;
}
for i in (len - third)..len {
scaled[i] = scaled[i] * ffn_scale;
}
scaled
}
fn apply_layer_wise_decay(&self, gradients: &Array1<T>) -> Array1<T> {
if (self.layer_wise_decay - T::one()).abs() < T::from(1e-12).expect("eps") {
return gradients.clone();
}
let len = gradients.len();
let chunk = len / self.num_layers;
if chunk == 0 {
return 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 = self.num_layers - 1 - 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;
}
}
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::from(self.gradient_accumulation_steps).expect("accum steps convert");
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);
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).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).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_bottom > lr_top,
"output layer should have highest LR (index 3), lr_top={}, lr_bottom={}",
lr_top,
lr_bottom
);
}
#[test]
fn test_nlp_optimizer_gradient_accumulation() {
let mut opt = NLPOptimizer::new(0.01_f64).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)
.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)
.with_token_aware_scaling(true)
.with_max_grad_norm(100.0)
.with_momentum(0.0);
let mut opt_base = NLPOptimizer::new(0.01_f64)
.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);
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);
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);
}
}