From dc2943b4ab6c986ec7562b9ff894b1cb3c3bab06 Mon Sep 17 00:00:00 2001 From: hconsulting987654321-blip Date: Sat, 11 Apr 2026 21:50:51 +0200 Subject: [PATCH] fix(epoch): emit decayed old bond entries absent from new in mat_ema_alpha_sparse The sparse EMA function `mat_ema_alpha_sparse` previously only emitted output entries present in the `new` matrix. Old bond entries that existed in `old_row` but were absent from `new_row` had their decayed values computed into `decayed_values` but never emitted, causing bonds to drop to zero instantly rather than decaying gradually via EMA. This diverged from the dense equivalent `mat_ema_alpha` and broke the intended bond decay behavior: when a validator removes their weight to a miner, the bond should decay by factor `(1 - alpha)` per epoch rather than being zeroed immediately. Fix: after processing new_row entries, iterate over decayed_values for columns absent from new_row and emit any non-zero decayed value. Output is sorted by column index to maintain the sparse row invariant. Also fix the corresponding test that was asserting the buggy behavior (expected `0.0` corrected to `0.9` for `alpha=0.1`, `old=1.0`). --- pallets/subtensor/src/epoch/math.rs | 11 +++++++++++ pallets/subtensor/src/tests/math.rs | 7 +++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pallets/subtensor/src/epoch/math.rs b/pallets/subtensor/src/epoch/math.rs index 6a53c9767b..b110e9b74d 100644 --- a/pallets/subtensor/src/epoch/math.rs +++ b/pallets/subtensor/src/epoch/math.rs @@ -1505,7 +1505,9 @@ pub fn mat_ema_alpha_sparse( // Add alpha_j * new_ij, clamp to [0, 1], and emit sparse entries > 0. let mut out_row: Vec<(u16, I32F32)> = Vec::new(); + let mut new_cols = std::collections::BTreeSet::new(); for &(j, new_val) in new_row.iter() { + new_cols.insert(j); if let (Some(&a), Some(&decayed)) = (alpha_row.get(j as usize), decayed_values.get(j as usize)) { @@ -1517,6 +1519,15 @@ pub fn mat_ema_alpha_sparse( } } + // Emit decayed old entries for columns absent from new_row. + // This ensures old bonds decay gradually rather than dropping to zero instantly. + for (j, &decayed) in decayed_values.iter().enumerate() { + if !new_cols.contains(&(j as u16)) && decayed > zero { + out_row.push((j as u16, decayed)); + } + } + out_row.sort_unstable_by_key(|&(j, _)| j); + result.push(out_row); } diff --git a/pallets/subtensor/src/tests/math.rs b/pallets/subtensor/src/tests/math.rs index 6591d975b0..a844091e28 100644 --- a/pallets/subtensor/src/tests/math.rs +++ b/pallets/subtensor/src/tests/math.rs @@ -2107,15 +2107,18 @@ fn test_math_sparse_mat_ema_alpha() { let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); let result = mat_ema_alpha_sparse(&new, &old, &alphas); assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); + // Old entry absent from new must decay via EMA rather than drop to zero instantly. + // With alpha=0.1: old[0][0]=1.0 absent from new => (1-0.1)*1.0 = 0.9 + // new[1][0]=2.0 absent from old => 0.1*2.0 = 0.2 let old: Vec = vec![1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; let new: Vec = vec![0., 0., 0., 0., 2., 0., 0., 0., 0., 0., 0., 0.]; - let target: Vec = vec![0.0, 0., 0., 0., 0.2, 0., 0., 0., 0., 0., 0., 0.]; + let target: Vec = vec![0.9, 0., 0., 0., 0.2, 0., 0., 0., 0., 0., 0., 0.]; let old = vec_to_sparse_mat_fixed(&old, 4, false); let new = vec_to_sparse_mat_fixed(&new, 4, false); let target = vec_to_sparse_mat_fixed(&target, 4, false); let alphas = vec_to_mat_fixed(&[0.1; 12], 4, false); let result = mat_ema_alpha_sparse(&new, &old, &alphas); - assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-1)); + assert_sparse_mat_compare(&result, &target, I32F32::from_num(1e-4)); } #[test]