fix: workflow-review findings — BN gamma init, ThresholdParams serde, init docs

Concurrent validation workflow (2 review lanes + adversarial verification,
13 agents): 5 confirmed findings, 3 refuted. Fixes:
- wiflow_std: pin BatchNorm gamma to 1.0 (tch default draws Uniform(0,1) —
  silently halves activations in from-scratch training; loaded checkpoints
  unaffected, parity re-verified after the change)
- wiflow_std: document the conv-init divergences vs the reference's
  effective kaiming_normal(fan_out) re-init (from-scratch dynamics only)
- ieee80211bf: ThresholdParams deserialization validates via try_from so
  the <=100 invariant holds for untrusted payloads (+ rejection test)

Benchmarks (release, ruvzen): GeometryEmbedding 1.84us/call (542k/s),
MAE tokenization 7.38us/window (135k/s), 802.11bf FSM 8.9M events/s —
nothing suspicious.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-06-10 20:45:11 -04:00
parent af6621005f
commit 64d95d2aad
4 changed files with 59 additions and 16 deletions
@@ -88,6 +88,13 @@ fn serde_rejects_out_of_range_setup_id() {
assert!(serde_json::from_str::<MeasurementSetupId>("127").is_ok());
}
#[test]
fn serde_rejects_out_of_range_threshold_params() {
assert!(serde_json::from_str::<ThresholdParams>(r#"{"delta_percent":255}"#).is_err());
let ok = serde_json::from_str::<ThresholdParams>(r#"{"delta_percent":100}"#).unwrap();
assert_eq!(ok.delta_percent(), 100);
}
// ---------- validation, no panics ----------
#[test]
@@ -216,11 +216,30 @@ pub fn bandwidth_mhz(bw: Bandwidth) -> u16 {
/// Threshold-based reporting parameters: a report is generated only when the
/// measurement changes by at least `delta_percent` relative to the last
/// reported measurement (normalized-change trigger).
///
/// Deserialization validates through [`ThresholdParams::new`] so the
/// `delta_percent <= 100` invariant holds on every construction path,
/// including untrusted wire/persisted payloads (same convention as
/// [`MeasurementSetupId`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "RawThresholdParams")]
pub struct ThresholdParams {
delta_percent: u8,
}
#[derive(Deserialize)]
struct RawThresholdParams {
delta_percent: u8,
}
impl TryFrom<RawThresholdParams> for ThresholdParams {
type Error = BfError;
fn try_from(raw: RawThresholdParams) -> Result<Self, Self::Error> {
Self::new(raw.delta_percent)
}
}
impl ThresholdParams {
pub fn new(delta_percent: u8) -> Result<Self, BfError> {
if delta_percent > 100 {
@@ -6,6 +6,17 @@ use tch::{nn, nn::Module, Tensor};
use super::config::{CONV_BLOCK_DROPOUT, TCN_KERNEL};
/// BatchNorm config matching the reference: gamma = 1 (PyTorch default; the
/// reference additionally pins BatchNorm1d weight=1/bias=0). tch-0.24's
/// `BatchNormConfig::default()` would draw gamma from Uniform(0,1), silently
/// halving activations on average in from-scratch training.
pub(super) fn bn_cfg() -> nn::BatchNormConfig {
nn::BatchNormConfig {
ws_init: nn::Init::Const(1.0),
..Default::default()
}
}
// ---------------------------------------------------------------------------
// GroupedTemporalBlock (TCN level)
// ---------------------------------------------------------------------------
@@ -53,19 +64,19 @@ impl GroupedTemporalBlock {
};
let conv1_group = nn::conv1d(&vs / "conv1_group", c_in, c_in, k, grouped_cfg(groups));
let bn1_group = nn::batch_norm1d(&vs / "bn1_group", c_in, Default::default());
let bn1_group = nn::batch_norm1d(&vs / "bn1_group", c_in, bn_cfg());
let conv1_pw = nn::conv1d(&vs / "conv1_pw", c_in, c_out, 1, pointwise_cfg);
let bn1_pw = nn::batch_norm1d(&vs / "bn1_pw", c_out, Default::default());
let bn1_pw = nn::batch_norm1d(&vs / "bn1_pw", c_out, bn_cfg());
let conv2_group = nn::conv1d(&vs / "conv2_group", c_out, c_out, k, grouped_cfg(groups));
let bn2_group = nn::batch_norm1d(&vs / "bn2_group", c_out, Default::default());
let bn2_group = nn::batch_norm1d(&vs / "bn2_group", c_out, bn_cfg());
let conv2_pw = nn::conv1d(&vs / "conv2_pw", c_out, c_out, 1, pointwise_cfg);
let bn2_pw = nn::batch_norm1d(&vs / "bn2_pw", c_out, Default::default());
let bn2_pw = nn::batch_norm1d(&vs / "bn2_pw", c_out, bn_cfg());
let downsample = (c_in != c_out).then(|| {
(
nn::conv1d(&vs / "ds_conv", c_in, c_out, 1, pointwise_cfg),
nn::batch_norm1d(&vs / "ds_bn", c_out, Default::default()),
nn::batch_norm1d(&vs / "ds_bn", c_out, bn_cfg()),
)
});
@@ -145,11 +156,11 @@ impl ConvBlock {
..Default::default()
};
let conv1 = nn::conv(&vs / "conv1", c_in, c_out, [1, 3], asym(stride_w));
let bn1 = nn::batch_norm2d(&vs / "bn1", c_out, Default::default());
let bn1 = nn::batch_norm2d(&vs / "bn1", c_out, bn_cfg());
let conv2 = nn::conv(&vs / "conv2", c_out, c_out, [1, 3], asym(1));
let bn2 = nn::batch_norm2d(&vs / "bn2", c_out, Default::default());
let bn2 = nn::batch_norm2d(&vs / "bn2", c_out, bn_cfg());
let conv3 = nn::conv(&vs / "conv3", c_out, c_out, [1, 3], asym(1));
let bn3 = nn::batch_norm2d(&vs / "bn3", c_out, Default::default());
let bn3 = nn::batch_norm2d(&vs / "bn3", c_out, bn_cfg());
let ds_conv = nn::conv(
&vs / "ds_conv",
@@ -162,7 +173,7 @@ impl ConvBlock {
..Default::default()
},
);
let ds_bn = nn::batch_norm2d(&vs / "ds_bn", c_out, Default::default());
let ds_bn = nn::batch_norm2d(&vs / "ds_bn", c_out, bn_cfg());
ConvBlock {
conv1,
@@ -228,9 +239,9 @@ impl AxialAttention {
..Default::default()
},
);
let bn_qkv = nn::batch_norm1d(&vs / "bn_qkv", planes * 3, Default::default());
let bn_similarity = nn::batch_norm2d(&vs / "bn_similarity", groups, Default::default());
let bn_output = nn::batch_norm1d(&vs / "bn_output", planes, Default::default());
let bn_qkv = nn::batch_norm1d(&vs / "bn_qkv", planes * 3, bn_cfg());
let bn_similarity = nn::batch_norm2d(&vs / "bn_similarity", groups, bn_cfg());
let bn_output = nn::batch_norm1d(&vs / "bn_output", planes, bn_cfg());
AxialAttention {
qkv,
@@ -2,8 +2,14 @@
//!
//! Idiomatic reimplementation of the DY2434 reference (Apache-2.0); see the
//! [module docs](crate::wiflow_std) for provenance and the evidence grade.
//! Weights are initialised from scratch (tch defaults; the axial-attention
//! qkv conv mirrors the reference's `N(0, sqrt(1/in_planes))` init). The
//! From-scratch init: BN gamma is pinned to 1 (see `layers::bn_cfg`); the
//! axial-attention qkv conv uses `N(0, sqrt(1/in_planes))` per the
//! reference's `attention.py` intent (note the reference's *effective* init
//! differs — its `_initialize_weights` re-inits every `nn.Conv1d`, qkv
//! included, with `kaiming_normal(fan_out)`); conv weights keep tch defaults
//! (kaiming-uniform fan_in), which differ in scale from PyTorch's defaults.
//! These divergences affect from-scratch training dynamics only — BN absorbs
//! them at init, and loaded checkpoints overwrite everything. The
//! retrained PyTorch checkpoint loads via [`WiFlowStdModel::load`] after
//! key-remapped safetensors export
//! (`benchmarks/wiflow-std/export_to_safetensors.py`); numerical parity with
@@ -99,9 +105,9 @@ impl WiFlowStdModel {
..Default::default()
},
);
let dec_bn1 = nn::batch_norm2d(&root / "dec_bn1", mid, Default::default());
let dec_bn1 = nn::batch_norm2d(&root / "dec_bn1", mid, super::layers::bn_cfg());
let dec_conv2 = nn::conv2d(&root / "dec_conv2", mid, 2, 1, Default::default());
let dec_bn2 = nn::batch_norm2d(&root / "dec_bn2", 2, Default::default());
let dec_bn2 = nn::batch_norm2d(&root / "dec_bn2", 2, super::layers::bn_cfg());
Ok(WiFlowStdModel {
vs,