Mamba M3.5: canonical numerically-stable initialization

Plain randn(0,1) init made A=-exp(A_log) and Δ wildly large, overflowing
the real exp(Δ·A) scan to NaN (the stub never cared — it discarded these
weights). new()/new_seeded() now share a build() with canonical S6 init:
  - A_log = ln(1..=d_state) ⇒ A = -(1..=d_state), bounded
  - dt_bias so softplus(dt_bias) ≈ 0.01 (small, stable Δ; near-identity
    scan at init — intentional for gradient flow)
  - D = 1, zero conv bias, projections scaled by 1/√fan_in (capped 0.5)

This fixes the NaN that broke omni-cortex's d231 action-conditioned
predictor training (now green). Seeded determinism preserved.

Tests: active_block helper (Δ overridden to ≈0.69) exercises the
scan-active regime so the liveness check can observe each weight; the
training test asserts a seed-varying backbone weight (conv1d_weight)
moves. All 6 selective-scan tests green incl. the finite-diff grad check.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
omar sobh
2026-06-03 11:30:33 +00:00
co-authored by Claude Opus 4.8
parent 33cf9bf731
commit 639937e9f5
2 changed files with 83 additions and 60 deletions
@@ -287,40 +287,7 @@ pub struct MambaBlock {
impl MambaBlock {
/// Create new `MambaBlock`
pub fn new(config: MambaConfig, device: &Device) -> Result<Self> {
let d_inner = config.get_d_inner();
let dt_rank = config.get_dt_rank();
// Initialize parameters
let in_proj = Tensor::randn(&[config.d_model, d_inner * 2], device)?;
let conv1d_weight = Tensor::randn(&[d_inner, 1, config.d_conv], device)?;
let conv1d_bias = if config.conv_bias {
Some(Tensor::randn(&[d_inner], device)?)
} else {
None
};
let A_log = Tensor::randn(&[d_inner, config.d_state], device)?;
let x_proj = Tensor::randn(&[d_inner, dt_rank + 2 * config.d_state], device)?;
let dt_proj = Tensor::randn(&[dt_rank, d_inner], device)?;
let dt_bias = Tensor::randn(&[d_inner], device)?;
let d_skip = Tensor::randn(&[d_inner], device)?;
let out_proj = Tensor::randn(&[d_inner, config.d_model], device)?;
let selective_scan = SelectiveScan::new(d_inner, config.d_state);
Ok(Self {
config,
device: device.clone(),
in_proj,
conv1d_weight,
conv1d_bias,
A_log,
x_proj,
dt_proj,
dt_bias,
d_skip,
out_proj,
selective_scan,
})
Self::build(config, device, None)
}
/// Create `MambaBlock` with a deterministic seed.
@@ -335,38 +302,70 @@ impl MambaBlock {
/// same operator-supplied `seed` produces a stable mapping across
/// the six (or seven, with conv_bias) internal tensors.
pub fn new_seeded(config: MambaConfig, device: &Device, seed: u64) -> Result<Self> {
let d_inner = config.get_d_inner();
let dt_rank = config.get_dt_rank();
Self::build(config, device, Some(seed))
}
// Six (or seven) per-tensor seeds derived from the operator
// seed. Using SplitMix64 so adjacent operator seeds don't
// produce correlated per-tensor seeds.
let mut state = seed;
let mut next_seed = || {
/// Shared constructor for [`Self::new`] (random) and
/// [`Self::new_seeded`] (`Some(seed)` ⇒ deterministic). Uses
/// canonical, numerically-stable selective-SSM initialization:
/// `A_log = ln(1..=d_state)` so `A = -(1..=d_state)` is bounded;
/// `dt_bias` set so `softplus(dt_bias) ≈ 0.01` (small, stable Δ);
/// `D = 1`; zero conv bias; projection weights scaled by `1/√fan_in`
/// (capped at 0.5). Plain `randn(0,1)` would overflow the real
/// `exp(Δ·A)` scan to NaN — the stub didn't care because it
/// discarded these weights.
fn build(config: MambaConfig, device: &Device, seed: Option<u64>) -> Result<Self> {
let d = config.get_d_inner();
let dt_rank = config.get_dt_rank();
let n = config.d_state;
let dbc = dt_rank + 2 * n;
let mut state = seed.unwrap_or(0xD1CE_F00D);
let mut rand_t = |shape: &[usize]| -> std::result::Result<Tensor, rtx_tensor::TensorError> {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
let s = z ^ (z >> 31);
match seed {
Some(_) => Tensor::randn_seeded(shape, device, s),
None => Tensor::randn(shape, device),
}
};
let scaled =
|t: Tensor, f: f32| -> std::result::Result<Tensor, rtx_tensor::TensorError> {
let mut v = t.to_vec()?;
for x in v.iter_mut() {
*x *= f;
}
Tensor::from_vec(v, t.shape().dims(), device)
};
let lin = |fan: usize| (1.0 / (fan.max(1) as f32).sqrt()).min(0.5);
let in_proj = Tensor::randn_seeded(&[config.d_model, d_inner * 2], device, next_seed())?;
let conv1d_weight =
Tensor::randn_seeded(&[d_inner, 1, config.d_conv], device, next_seed())?;
let in_proj = scaled(rand_t(&[config.d_model, 2 * d])?, lin(config.d_model))?;
let conv1d_weight = scaled(rand_t(&[d, 1, config.d_conv])?, 0.2)?;
let conv1d_bias = if config.conv_bias {
Some(Tensor::randn_seeded(&[d_inner], device, next_seed())?)
Some(Tensor::from_vec(vec![0.0f32; d], &[d], device)?)
} else {
None
};
let A_log = Tensor::randn_seeded(&[d_inner, config.d_state], device, next_seed())?;
let x_proj =
Tensor::randn_seeded(&[d_inner, dt_rank + 2 * config.d_state], device, next_seed())?;
let dt_proj = Tensor::randn_seeded(&[dt_rank, d_inner], device, next_seed())?;
let dt_bias = Tensor::randn_seeded(&[d_inner], device, next_seed())?;
let d_skip = Tensor::randn_seeded(&[d_inner], device, next_seed())?;
let out_proj = Tensor::randn_seeded(&[d_inner, config.d_model], device, next_seed())?;
// A = -exp(A_log) = -(1..=d_state) per channel (S4D-real init).
let mut a_v = vec![0.0f32; d * n];
for j in 0..d {
for nn in 0..n {
a_v[j * n + nn] = ((nn + 1) as f32).ln();
}
}
let a_log = Tensor::from_vec(a_v, &[d, n], device)?;
let x_proj = scaled(rand_t(&[d, dbc])?, lin(d))?;
let dt_proj = scaled(rand_t(&[dt_rank, d])?, lin(dt_rank))?;
// softplus(dt_bias) ≈ 0.01 ⇒ small, stable time-step.
let dt_bias_val = (0.01f32.exp() - 1.0).ln();
let dt_bias = Tensor::from_vec(vec![dt_bias_val; d], &[d], device)?;
let d_skip = Tensor::from_vec(vec![1.0f32; d], &[d], device)?;
let out_proj = scaled(rand_t(&[d, config.d_model])?, lin(d))?;
let selective_scan = SelectiveScan::new(d_inner, config.d_state);
let selective_scan = SelectiveScan::new(d, config.d_state);
Ok(Self {
config,
@@ -374,7 +373,7 @@ impl MambaBlock {
in_proj,
conv1d_weight,
conv1d_bias,
A_log,
A_log: a_log,
x_proj,
dt_proj,
dt_bias,