mirror of
https://github.com/ruvnet/RuView
synced 2026-08-07 20:01:43 +00:00
17471e93ff
* feat(calibration): NodeGeometry transceiver-geometry recording (ADR-152 §2.1.1) PerceptAlign-motivated geometry capture at enrollment: per-node optional records (position, antenna orientation, inter-node distances, acquisition method) — recorded when known, never required. Event-sourced via EnrollmentEvent::GeometryRecorded (latest recording wins); persisted on SpecialistBank with serde defaults so pre-ADR-152 bank JSON loads cleanly (fixture-proven, and geometry-free banks serialize byte-shape-identical to the old schema); threaded through MultiNodeMixture as data only — the learned geometry embeddings and algorithmic fusion use are §2.1.2, deliberately deferred until the ADR-151 P6 LoRA heads exist. Geometry recorded from now on means banks captured today remain usable for layout-conditioned training later — you can't retroactively add geometry to data you didn't record. 8 new tests (3 geometry, 2 anchor, 2 bank, 1 multistatic) + full-loop extension (2-node geometry, one tape-measured + one unknown, surviving the bank JSON round-trip the runtime loads from). 50/50 calibration (both feature configs) + 23 CLI tests green. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(training): two-checkerboard camera↔room calibration for ADR-079 labels (ADR-152 §2.1.3) Defends the camera-supervised pipeline against PerceptAlign's "coordinate overfitting": MediaPipe keypoints were emitted in raw camera coordinates with no shared frame and no transceiver-geometry metadata — the exact label shape that memorizes deployment layout and collapses cross-layout. - scripts/calibrate-camera-room.py + calibration_lib.py: OpenCV two-checkerboard calibration → versioned bundle JSON (intrinsics, camera→room extrinsics, checkerboard spec, transceiver geometry, sha256 calibration_id). Intrinsics resolve from file > cache > multi-view computation > loud-warning 2-view fallback. - collect-ground-truth.py --calibration <bundle>: every sample gains keypoints_room (unit bearing rays from the camera center in the room frame — documented projective alignment; raw image coords preserved so training chooses), camera_origin_room, calibration_id, and the transceiver geometry stamp. Without the flag, output is byte-identical to before (tested) + a one-line ADR-152 warning. Design finding (recorded for ADR-152): a single planar checkerboard's corner grid is centrosymmetric — the reversed corner ordering fits a ghost camera pose with IDENTICAL reprojection error, so per-board flip disambiguation is mathematically ill-posed. solve_two_board_extrinsics solves the joint wall+floor set over all 4 flip combinations, where the minimum is unique — an independent reason the TWO-checkerboard method is required, beyond what PerceptAlign states. 15 headless pytest tests green (synthetic corners: extrinsics recovery incl. ghost resolution, bundle round-trip + hash stability, ray transforms w/ distortion + cross-resolution, no-calibration byte identity). Co-Authored-By: RuFlo <ruv@ruv.net> * feat(benchmarks): WiFlow-STD reproduction harness + measurement (a) results (ADR-152 §2.2) Shipped checkpoint REFUTED (0.08% PCK@20, wrong keypoint normalization); 6 reproducibility defects documented (broken imports, corrupted dataset tail with float32-max garbage that NaN-poisons fp16 BatchNorm, unreachable test phase). After repairs, retraining with upstream defaults reproduces 96.09% PCK@20 full-test / 96.61% corruption-free (published 97.25%) on RTX 5080. Claims graded MEASURED-EQUIVALENT; 2.23M params + ~0.055 GFLOPs verified. Third-party code/weights/data stay out of tree (gitignored). Co-Authored-By: claude-flow <ruv@ruv.net> * feat: ADR-152 Rust integrations + ADR-153 802.11bf protocol model - calibration: GeometryEmbedding — 32-slot permutation-invariant NodeGeometry featurization for future LoRA-head conditioning (ADR-152 §2.1.2); derived SpecialistBank::geometry_embedding() accessor; 59 tests - train: MaePretrainConfig + patchify/random-mask with UNSW measured recipe (80% masking, (30,3) patches; ADR-152 §2.3, arXiv 2511.18792); strict no-truncate/no-NaN policy; proptest properties - train: WiFlowStdModel — tch-gated port of the verified ~96%-PCK@20 WiFlow-STD architecture (ADR-152 §2.2 beyond-SOTA); ungated param formula pinned to 2,225,042; 15/17-keypoint support; 239 crate tests - hardware: ieee80211bf forward-compatibility protocol model (ADR-153): SpecProfile gates, SensingCapabilities negotiation, required ConsentMode, session FSM, SensingTransport + SimTransport + OpportunisticCsiBridge; full acceptance checklist covered; 156+4 tests - deps: ruvector bumps per ADR-152 §2.6 survey (mincut/solver 2.0.6, attention 2.1.0, gnn 2.2.0); vendor/ruvector synced to a083bd77f - docs: ADR-153 accepted; ADR-152 §2.2 status, §2.4 amendment, §2.6 added Workspace: 162 test suites green (--no-default-features); Python proof PASS. Known pre-existing flake: homecore-api env_empty_falls_back_to_defaults (unserialized env-var mutation) — untouched, follow-up. Co-Authored-By: claude-flow <ruv@ruv.net> * docs: CHANGELOG + CLAUDE.md entries for ADR-152 integrations and ADR-153 Co-Authored-By: claude-flow <ruv@ruv.net> * fix(train): repair tch-backend bit-rot — gated path compiles and tests run again Mechanical API refresh against current tch: Vec::from(Tensor) -> try_from (+ explicit flatten), numel() usize cast, Rem/div ops -> remainder() / divide_scalar_mode(floor) — the latter fixed a silent true-division bug in heatmap argmax decoding; clamp(1.0, f64::MAX) -> clamp_min (torch 2.x scalar overflow panic); petgraph EdgeRef import; missing EvalMetrics and verify_checkpoint_dir APIs that tests documented. wiflow_std roundtrip test uses safetensors (.pt _save_parameters roundtrip broken in torch 2.11 Windows). Gated: 349 passed (incl. all 20 wiflow_std); ungated: unchanged. Known pre-existing: gaussian-heatmap convention mismatch (2 tests), proof seed race under parallel threads — documented, deliberate follow-ups. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(train): WiFlow-STD PyTorch->tch weight import + numerical parity proof export_to_safetensors.py maps the retrained checkpoint (295 tensors -> 248 mapped, param sum exactly 2,225,042; num_batches_tracked dropped) into a tch-loadable safetensors plus a deterministic parity fixture. Gated #[ignore] integration test loads it strictly and asserts forward-pass agreement: max abs diff 1.192e-7 on the seed-42 fixture. dump_variable_names test makes the tch name layout authoritative. Zero architecture discrepancies found. Co-Authored-By: claude-flow <ruv@ruv.net> * 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> * docs(adr): ADR-152 §2.1.4 gate resolved — PerceptAlign repo MIT, dataset on HF Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): edge optimization measured + measurement (b) blocked + 92.9% retraction Edge optimization (ADR-152 optimize track): ONNX Runtime fp32 is the CPU latency win (3.2 ms/window, ~3.4x faster than torch, parity 2.4e-7); ORT dynamic int8 reaches 2.44 MB (paper's ~2.2 MB claim plausible only via conv-capable toolchains; -0.16pt PCK@20, +18% MPJPE, 2x slower); torch dynamic quant converts 0% of this conv-only model; fp16 halves storage free but is slower on CPU. Measurement (b) BLOCKED-ON-DATA: only 1,077 paired ESP32 windows exist (stop rule <2k). Forensic recheck of the surviving April holdout RETRACTS the ADR-079 '92.9% PCK@20' figure: constant-output model, absolute (not torso) threshold, 69 near-static frames — mean predictor scores 100% under that protocol; torso-PCK@20 is 19.1%. Corroborates PR #535. Stale citations removed from user-guide, readme-details, ADR-152 §2.1.3; no-citation rule extended to ADR-079 accuracy claims. Unblock: >=2k-window multi-pose paired session + torso-PCK re-baseline. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(user-guide): corrected camera-supervised collection tutorial Step 0 CSI-rate check + session-length math (window yield = frames/20 — the May session's 8x under-delivery was a ~12 Hz CSI rate, not an aligner bug); two-checkerboard calibration step (ADR-152 §2.1.3); pose-variety and confidence guidance; torso-normalized PCK + temporal-split + pred-variance eval protocol (lessons from the 92.9% retraction); scale presets re-keyed to realistic window counts. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): static PTQ int8 (calibrated) results + overnight capture script Conv-only static QDQ beats dynamic int8 on accuracy (PCK@20 96.61-96.63% vs 96.52%, MPJPE +10% vs +18% over fp32) at ~equal size/latency; all-ops QDQ strictly worse (int8 activations through attention glue). Entropy calibration verified bit-identical to MinMax on this data. Deployment: ONNX fp32 for speed (3.2ms), static conv-only QDQ for smallest (2.53MB). Also: scripts/overnight-empty-capture.py — segmented UDP CSI recorder for empty-room baselines (no glob collisions, detach-safe). Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): measurement (b) MEASURED — optimization transfer only, mean-pose baseline wins WiFlow-STD fine-tuned on 2,046 fresh single-room ESP32 paired windows (temporal 70/15/15, 70->540 adapter, K=17): pretrained-init 65% PCK@20 vs scratch 0% (optimization transfer) but frozen-trunk ~0% (no feature transfer), and NOTHING beats the mean-pose baseline (95.9% PCK@20 — single subject, near-static normalized coords). Honesty gates held: pred std 0.0113 (non-constant model) but mean-baseline dominance means no citable CSI->pose capability from this data. ADR-152 open question 1 answered partially; definitive answer needs multi-subject/position data. Two new aligner findings: heterogeneous csi_shape with silent zero-padding (~20%), and extractCsiMatrix's transposed shape label (frame-major data, [nSc, nFrames] label) — fixes pending. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(benchmarks): efficiency sweep MEASURED — half model dominates full reference Compact WiFlow-STD variants on the same data/split/protocol: half (843,834 params, 0.38x) strictly dominates the 2.23M reference (PCK@20 96.62 vs 96.61, PCK@50 99.47 vs 99.11, MPJPE 0.00898 vs 0.0094) — the published architecture is over-parameterized for its own benchmark. quarter (338k) 96.05%; tiny (56,290 params, 1/39.5) holds 94.11% — a ~220KB fp32 edge candidate. In-domain caveats recorded; cross-domain untested. Co-Authored-By: claude-flow <ruv@ruv.net> * feat(train): compact WiFlow-STD presets in Rust + tiny edge artifact (ADR-152) WiFlowStdConfig gains half()/quarter()/tiny() mirroring the overnight sweep exactly: TcnGroupsMode (Fixed/Gcd/Depthwise), input_pw_groups, derived stride schedule and decoder-mid (all default to upstream behavior; legacy serde JSON unaffected). Param formulas pin to trained ground truth first try: 843,834 / 338,600 / 56,290; default 2,225,042 pin and 1.192e-7 parity unchanged. 248 tests green. Tiny edge artifact (tiny_edge_bench.py): ONNX fp32 = 295 KB, 0.66 ms/win (~1,500/s CPU), 94.11% PCK@20 (matches sweep clean-test exactly; parity 1.49e-7). Static int8 is a bad trade at this scale (-1.43pt, +19% MPJPE, -16% size, slower) — recorded as negative result. Export note: width-16 breaks AdaptiveAvgPool((15,1)) TorchScript export; replaced by exact mean+matmul equivalent, proven by parity. Co-Authored-By: claude-flow <ruv@ruv.net> * fix: resolve all 10 confirmed code-review findings (7-angle review, 20/20 verified) wiflow_std: min_feature_width (default 15) replaces the keypoints->stride coupling — for_keypoints(17) now provably builds the trained [2,2,2,2] graph and pools 15->17, matching the validated Python protocol (pinned by tests); param_count() total on invalid configs; random_mask returns Result and rejects non-finite/out-of-range ratios; trainer checkpoints switched to safetensors (.pt VarStore roundtrip broken on Windows torch 2.11). ieee80211bf: SBP proxy now re-triggers instances and relays reports via Action::RelaySbpReport -> SensingFrame::SbpReport (clients consume via their existing path); missed_instances reset on success = consecutive semantics; SessionTable gains a guarded SBP entry point + unknown-id drop counter; initiator-role sessions reject inbound setup/SBP requests (RejectedNotSupported) closing the idle hijack; StartSetup/StartSbp outside Idle return InvalidStateForCommand; SBP validation unified through evaluate_setup with a 1:1 SetupStatus->SbpStatus mapping. events.rs split out to honor the 500-line cap. calibration/cli: enrollment geometry now actually reaches trained banks — both production call sites attach .with_geometry; --geometry flag on train-room and POST /enroll/geometry + train-body geometry on calibrate-serve give production a recording surface; geometry-free banks log the ADR-152 §2.1.2 note. benchmarks: corruption masks committed as ground truth (unregenerable after in-place cleaning; verified bit-identical regeneration from the pristine copy) + generate_corruption_masks.py producer; _bench_common.py dedups the 5x-copied shim/evaluate/seed/remap (post-refactor PCK@20 re-verified equal to the last digit); remote scripts get the mmap patch; tiny_edge --calib validated multiple-of-64; onnx_bench --help no longer executes (and overwrote) the export — artifact restored byte-exact. Workspace: 2,963 tests passed, 0 failed; Python proof PASS. Co-Authored-By: claude-flow <ruv@ruv.net> * ci: build workspace tests without debuginfo — runner disk exhaustion The combined 38-crate debug target exceeds the GitHub runner's disk ('final link failed: No space left on device'); the same tree measured 151GB locally with full debuginfo. CARGO_PROFILE_{DEV,TEST}_DEBUG=0 shrinks the target ~5-10x; debuginfo serves no purpose in CI test runs. Co-Authored-By: claude-flow <ruv@ruv.net>
335 lines
11 KiB
Rust
335 lines
11 KiB
Rust
//! Building-block layers for the WiFlow-STD model (tch backend, ADR-152 §2.2):
|
||
//! grouped causal TCN blocks, asymmetric residual conv blocks, and dual axial
|
||
//! attention. Internal to [`super::model`]; see the module docs for provenance.
|
||
|
||
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)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// One TCN level: two (depthwise-grouped causal conv → BN → SiLU → pointwise
|
||
/// conv → BN → SiLU → dropout) stages with a residual connection (1×1 + BN
|
||
/// projection when channels change) and a final SiLU.
|
||
///
|
||
/// Causality: each grouped conv pads by `(k-1)·dilation` and the trailing
|
||
/// padding is chomped off afterwards, exactly like the reference `Chomp1d`.
|
||
pub(super) struct GroupedTemporalBlock {
|
||
conv1_group: nn::Conv1D,
|
||
bn1_group: nn::BatchNorm,
|
||
conv1_pw: nn::Conv1D,
|
||
bn1_pw: nn::BatchNorm,
|
||
conv2_group: nn::Conv1D,
|
||
bn2_group: nn::BatchNorm,
|
||
conv2_pw: nn::Conv1D,
|
||
bn2_pw: nn::BatchNorm,
|
||
downsample: Option<(nn::Conv1D, nn::BatchNorm)>,
|
||
dropout: f64,
|
||
}
|
||
|
||
impl GroupedTemporalBlock {
|
||
/// `g_in`/`g_out`: group counts of the two grouped convs (each conv
|
||
/// groups over its own channel count — they differ under the ADR-152
|
||
/// compact variants' `Gcd`/`Depthwise` modes). `pw_groups` groups the
|
||
/// first pointwise conv and the residual projection (`input_pw_groups`
|
||
/// on block 0; 1 everywhere else).
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(super) fn new(
|
||
vs: nn::Path,
|
||
c_in: i64,
|
||
c_out: i64,
|
||
dilation: i64,
|
||
g_in: i64,
|
||
g_out: i64,
|
||
pw_groups: i64,
|
||
dropout: f64,
|
||
) -> Self {
|
||
let k = TCN_KERNEL as i64;
|
||
let padding = (k - 1) * dilation;
|
||
let grouped_cfg = |groups| nn::ConvConfig {
|
||
padding,
|
||
dilation,
|
||
groups,
|
||
bias: false,
|
||
..Default::default()
|
||
};
|
||
let pointwise_cfg = |groups| nn::ConvConfig {
|
||
groups,
|
||
bias: false,
|
||
..Default::default()
|
||
};
|
||
|
||
let conv1_group = nn::conv1d(&vs / "conv1_group", c_in, c_in, k, grouped_cfg(g_in));
|
||
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(pw_groups));
|
||
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(g_out));
|
||
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(1));
|
||
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(pw_groups)),
|
||
nn::batch_norm1d(&vs / "ds_bn", c_out, bn_cfg()),
|
||
)
|
||
});
|
||
|
||
GroupedTemporalBlock {
|
||
conv1_group,
|
||
bn1_group,
|
||
conv1_pw,
|
||
bn1_pw,
|
||
conv2_group,
|
||
bn2_group,
|
||
conv2_pw,
|
||
bn2_pw,
|
||
downsample,
|
||
dropout,
|
||
}
|
||
}
|
||
|
||
pub(super) fn forward_t(&self, x: &Tensor, train: bool) -> Tensor {
|
||
let res = match &self.downsample {
|
||
Some((conv, bn)) => conv.forward(x).apply_t(bn, train),
|
||
None => x.shallow_clone(),
|
||
};
|
||
let t = x.size()[2];
|
||
|
||
// Stage 1: grouped causal conv (chomp trailing padding) + pointwise.
|
||
let out = self
|
||
.conv1_group
|
||
.forward(x)
|
||
.narrow(2, 0, t) // Chomp1d
|
||
.apply_t(&self.bn1_group, train)
|
||
.silu()
|
||
.apply(&self.conv1_pw)
|
||
.apply_t(&self.bn1_pw, train)
|
||
.silu()
|
||
.dropout(self.dropout, train);
|
||
|
||
// Stage 2.
|
||
let out = self
|
||
.conv2_group
|
||
.forward(&out)
|
||
.narrow(2, 0, t) // Chomp1d
|
||
.apply_t(&self.bn2_group, train)
|
||
.silu()
|
||
.apply(&self.conv2_pw)
|
||
.apply_t(&self.bn2_pw, train)
|
||
.silu()
|
||
.dropout(self.dropout, train);
|
||
|
||
(out + res).silu()
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ConvBlock (ConvBlock1 / AsymmetricConvBlock)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Asymmetric residual conv block: three `(1, 3)` convs (only the subcarrier
|
||
/// axis is convolved) with BN, SiLU and channel dropout, plus a 1×1 + BN
|
||
/// residual projection. `stride_w == 1` reproduces the reference `ConvBlock1`,
|
||
/// `stride_w == 2` the downsampling `AsymmetricConvBlock`.
|
||
pub(super) struct ConvBlock {
|
||
conv1: nn::Conv2D,
|
||
bn1: nn::BatchNorm,
|
||
conv2: nn::Conv2D,
|
||
bn2: nn::BatchNorm,
|
||
conv3: nn::Conv2D,
|
||
bn3: nn::BatchNorm,
|
||
ds_conv: nn::Conv2D,
|
||
ds_bn: nn::BatchNorm,
|
||
}
|
||
|
||
impl ConvBlock {
|
||
pub(super) fn new(vs: nn::Path, c_in: i64, c_out: i64, stride_w: i64) -> Self {
|
||
let asym = |stride_w| nn::ConvConfigND::<[i64; 2]> {
|
||
stride: [1, stride_w],
|
||
padding: [0, 1],
|
||
..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, 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, 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, bn_cfg());
|
||
|
||
let ds_conv = nn::conv(
|
||
&vs / "ds_conv",
|
||
c_in,
|
||
c_out,
|
||
[1, 1],
|
||
nn::ConvConfigND::<[i64; 2]> {
|
||
stride: [1, stride_w],
|
||
bias: false,
|
||
..Default::default()
|
||
},
|
||
);
|
||
let ds_bn = nn::batch_norm2d(&vs / "ds_bn", c_out, bn_cfg());
|
||
|
||
ConvBlock {
|
||
conv1,
|
||
bn1,
|
||
conv2,
|
||
bn2,
|
||
conv3,
|
||
bn3,
|
||
ds_conv,
|
||
ds_bn,
|
||
}
|
||
}
|
||
|
||
pub(super) fn forward_t(&self, x: &Tensor, train: bool) -> Tensor {
|
||
let identity = self.ds_conv.forward(x).apply_t(&self.ds_bn, train);
|
||
let out = x
|
||
.apply(&self.conv1)
|
||
.apply_t(&self.bn1, train)
|
||
.silu()
|
||
.feature_dropout(CONV_BLOCK_DROPOUT, train) // Dropout2d
|
||
.apply(&self.conv2)
|
||
.apply_t(&self.bn2, train)
|
||
.silu()
|
||
.feature_dropout(CONV_BLOCK_DROPOUT, train)
|
||
.apply(&self.conv3)
|
||
.apply_t(&self.bn3, train);
|
||
(out + identity).silu()
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Axial attention
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Single-axis self-attention with BN-normalised qkv, BN-normalised
|
||
/// similarity logits and BN-normalised output. `width == true` attends along
|
||
/// the last (W) axis, otherwise along the H axis; the other spatial axis is
|
||
/// folded into the batch.
|
||
pub(super) struct AxialAttention {
|
||
qkv: nn::Conv1D,
|
||
bn_qkv: nn::BatchNorm,
|
||
bn_similarity: nn::BatchNorm,
|
||
bn_output: nn::BatchNorm,
|
||
out_planes: i64,
|
||
groups: i64,
|
||
width: bool,
|
||
}
|
||
|
||
impl AxialAttention {
|
||
pub(super) fn new(vs: nn::Path, planes: i64, groups: i64, width: bool) -> Self {
|
||
// Reference init: N(0, sqrt(1 / in_planes)).
|
||
let qkv = nn::conv1d(
|
||
&vs / "qkv",
|
||
planes,
|
||
planes * 3,
|
||
1,
|
||
nn::ConvConfig {
|
||
bias: false,
|
||
ws_init: nn::Init::Randn {
|
||
mean: 0.0,
|
||
stdev: (1.0 / planes as f64).sqrt(),
|
||
},
|
||
..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,
|
||
bn_qkv,
|
||
bn_similarity,
|
||
bn_output,
|
||
out_planes: planes,
|
||
groups,
|
||
width,
|
||
}
|
||
}
|
||
|
||
pub(super) fn forward_t(&self, x: &Tensor, train: bool) -> Tensor {
|
||
// Fold the non-attended spatial axis into the batch:
|
||
// width: [B,C,H,W] → [B,H,C,W]; height: [B,C,H,W] → [B,W,C,H].
|
||
let x = if self.width {
|
||
x.permute([0, 2, 1, 3])
|
||
} else {
|
||
x.permute([0, 3, 1, 2])
|
||
};
|
||
let (n, outer, c, axis) = {
|
||
let s = x.size();
|
||
(s[0], s[1], s[2], s[3])
|
||
};
|
||
let flat = x.contiguous().view([n * outer, c, axis]);
|
||
|
||
// BN-normalised qkv: [N', 3·C, axis] → grouped q, k, v.
|
||
let gp = self.out_planes / self.groups; // group planes
|
||
let qkv = flat.apply(&self.qkv).apply_t(&self.bn_qkv, train).reshape([
|
||
n * outer,
|
||
3,
|
||
self.groups,
|
||
gp,
|
||
axis,
|
||
]);
|
||
let q = qkv.select(1, 0); // [N', g, gp, axis]
|
||
let k = qkv.select(1, 1);
|
||
let v = qkv.select(1, 2);
|
||
|
||
// similarity[b,g,i,j] = Σ_c q[b,g,c,i]·k[b,g,c,j], BN over the g maps.
|
||
let logits = q.transpose(2, 3).matmul(&k); // [N', g, axis, axis]
|
||
let similarity = logits
|
||
.apply_t(&self.bn_similarity, train)
|
||
.softmax(-1, logits.kind());
|
||
|
||
// out[b,g,c,i] = Σ_j similarity[b,g,i,j]·v[b,g,c,j].
|
||
let sv = v.matmul(&similarity.transpose(2, 3)); // [N', g, gp, axis]
|
||
let out = sv
|
||
.reshape([n * outer, self.out_planes, axis])
|
||
.apply_t(&self.bn_output, train)
|
||
.view([n, outer, self.out_planes, axis]);
|
||
|
||
// Restore [B, C, H, W].
|
||
if self.width {
|
||
out.permute([0, 2, 1, 3])
|
||
} else {
|
||
out.permute([0, 2, 3, 1])
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Width-axis then height-axis axial attention (the reference
|
||
/// `DualAxialAttention`, stride 1).
|
||
pub(super) struct DualAxialAttention {
|
||
width_axis: AxialAttention,
|
||
height_axis: AxialAttention,
|
||
}
|
||
|
||
impl DualAxialAttention {
|
||
pub(super) fn new(vs: nn::Path, planes: i64, groups: i64) -> Self {
|
||
DualAxialAttention {
|
||
width_axis: AxialAttention::new(&vs / "width", planes, groups, true),
|
||
height_axis: AxialAttention::new(&vs / "height", planes, groups, false),
|
||
}
|
||
}
|
||
|
||
pub(super) fn forward_t(&self, x: &Tensor, train: bool) -> Tensor {
|
||
let x = self.width_axis.forward_t(x, train);
|
||
self.height_axis.forward_t(&x, train)
|
||
}
|
||
}
|