feat(ml): add RMSNorm to distributional-dueling network

Add RMSNorm (root mean square normalization) after each hidden layer
in the distributional-dueling Q-network: shared backbone layers, value
stream, and advantage stream. RMSNorm stabilizes activations and
gradients without the overhead of full LayerNorm (no mean centering),
making the network less sensitive to input scale during training.

Architecture per layer: Linear -> LeakyReLU -> RMSNorm

RMSNorm weights are automatically tracked in the existing VarMap since
they are created via VarBuilder::from_varmap with the same shared map.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-07 11:19:33 +01:00
parent 1b40fecccd
commit a16994b0ff

View File

@@ -44,6 +44,7 @@ use candle_nn::{Linear, Module, VarBuilder, VarMap};
use serde::{Deserialize, Serialize};
use crate::dqn::mixed_precision::training_dtype;
use crate::dqn::rmsnorm::RMSNorm;
use crate::dqn::xavier_init::linear_xavier;
use crate::MLError;
@@ -122,14 +123,23 @@ pub struct DistributionalDuelingQNetwork {
/// Shared feature extraction layers
shared_layers: Vec<Linear>,
/// RMSNorm after each shared hidden layer
shared_norms: Vec<RMSNorm>,
/// Value stream layers (outputs distribution)
value_fc: Linear,
value_out: Linear, // Output: [batch, num_atoms]
/// RMSNorm after value stream hidden layer
value_norm: RMSNorm,
/// Advantage stream layers (outputs distributions per action)
advantage_fc: Linear,
advantage_out: Linear, // Output: [batch, num_actions * num_atoms]
/// RMSNorm after advantage stream hidden layer
advantage_norm: RMSNorm,
/// Configuration
config: DistributionalDuelingConfig,
@@ -155,8 +165,9 @@ impl DistributionalDuelingQNetwork {
let vars = VarMap::new();
let var_builder = VarBuilder::from_varmap(&vars, training_dtype(&device), &device);
// Build shared feature layers
// Build shared feature layers with RMSNorm after each
let mut shared_layers = Vec::new();
let mut shared_norms = Vec::new();
let mut current_dim = config.state_dim;
for (i, &hidden_dim) in config.shared_hidden_dims.iter().enumerate() {
@@ -166,6 +177,11 @@ impl DistributionalDuelingQNetwork {
MLError::ModelError(format!("Failed to Xavier init shared layer {}: {}", i, e))
})?;
shared_layers.push(layer);
let norm_name = format!("shared_rmsnorm_{}", i);
let norm = RMSNorm::new_default(var_builder.pp(&norm_name), hidden_dim)?;
shared_norms.push(norm);
current_dim = hidden_dim;
}
@@ -173,6 +189,8 @@ impl DistributionalDuelingQNetwork {
let value_fc_vb = var_builder.pp("value_fc");
let value_fc = linear_xavier(current_dim, config.value_hidden_dim, value_fc_vb)
.map_err(|e| MLError::ModelError(format!("Failed to Xavier init value_fc: {}", e)))?;
let value_norm =
RMSNorm::new_default(var_builder.pp("value_rmsnorm"), config.value_hidden_dim)?;
let value_out_vb = var_builder.pp("value_out");
let value_out = linear_xavier(config.value_hidden_dim, config.num_atoms, value_out_vb)
@@ -184,6 +202,10 @@ impl DistributionalDuelingQNetwork {
.map_err(|e| {
MLError::ModelError(format!("Failed to Xavier init advantage_fc: {}", e))
})?;
let advantage_norm = RMSNorm::new_default(
var_builder.pp("advantage_rmsnorm"),
config.advantage_hidden_dim,
)?;
let advantage_out_vb = var_builder.pp("advantage_out");
let advantage_out = linear_xavier(
@@ -197,10 +219,13 @@ impl DistributionalDuelingQNetwork {
Ok(Self {
shared_layers,
shared_norms,
value_fc,
value_out,
value_norm,
advantage_fc,
advantage_out,
advantage_norm,
config,
vars,
device,
@@ -234,9 +259,14 @@ impl DistributionalDuelingQNetwork {
.dim(0)
.map_err(|e| MLError::ModelError(format!("Failed to get batch size: {}", e)))?;
// Shared feature extraction
// Shared feature extraction: Linear → LeakyReLU → RMSNorm
let mut h = state.clone();
for (i, layer) in self.shared_layers.iter().enumerate() {
for (i, (layer, norm)) in self
.shared_layers
.iter()
.zip(self.shared_norms.iter())
.enumerate()
{
h = layer.forward(&h).map_err(|e| {
MLError::ModelError(format!("Shared layer {} forward failed: {}", i, e))
})?;
@@ -245,26 +275,37 @@ impl DistributionalDuelingQNetwork {
h = candle_nn::ops::leaky_relu(&h, self.config.leaky_relu_alpha).map_err(|e| {
MLError::ModelError(format!("LeakyReLU failed at shared layer {}: {}", i, e))
})?;
// RMSNorm stabilizes activations and gradients
h = norm.forward(&h).map_err(|e| {
MLError::ModelError(format!("RMSNorm failed at shared layer {}: {}", i, e))
})?;
}
// Value stream: V(s) → [batch, num_atoms]
// Value stream: Linear → LeakyReLU → RMSNorm → Linear → [batch, num_atoms]
let v = self.value_fc.forward(&h).map_err(|e| {
MLError::ModelError(format!("Value FC forward failed: {}", e))
})?;
let v = candle_nn::ops::leaky_relu(&v, self.config.leaky_relu_alpha).map_err(|e| {
MLError::ModelError(format!("Value LeakyReLU failed: {}", e))
})?;
let v = self.value_norm.forward(&v).map_err(|e| {
MLError::ModelError(format!("Value RMSNorm failed: {}", e))
})?;
let v = self.value_out.forward(&v).map_err(|e| {
MLError::ModelError(format!("Value output forward failed: {}", e))
})?; // [batch, num_atoms]
// Advantage stream: A(s,a) → [batch, num_actions * num_atoms]
// Advantage stream: Linear → LeakyReLU → RMSNorm → Linear → [batch, num_actions * num_atoms]
let a = self.advantage_fc.forward(&h).map_err(|e| {
MLError::ModelError(format!("Advantage FC forward failed: {}", e))
})?;
let a = candle_nn::ops::leaky_relu(&a, self.config.leaky_relu_alpha).map_err(|e| {
MLError::ModelError(format!("Advantage LeakyReLU failed: {}", e))
})?;
let a = self.advantage_norm.forward(&a).map_err(|e| {
MLError::ModelError(format!("Advantage RMSNorm failed: {}", e))
})?;
let a_flat = self.advantage_out.forward(&a).map_err(|e| {
MLError::ModelError(format!("Advantage output forward failed: {}", e))
})?; // [batch, num_actions * num_atoms]