Files
ruvnet--RuView/benchmarks/wiflow-std/RESULTS.md
T
ruv 575ee4d2eb 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>
2026-06-10 23:04:38 -04:00

15 KiB
Raw Blame History

WiFlow-STD (DY2434) Benchmark Results — ADR-152 §2.2

Upstream: https://github.com/DY2434/WiFlow-WiFi-Pose-Estimation-with-Spatio-Temporal-Decoupling pinned at 06899d29 (2026-04-05), Apache-2.0. Dataset: Kaggle kaka2434/wiflow-dataset (12.8 GB archive → 15.5 GB extracted; 360,000 windows of 540×20 CSI + 15-keypoint 2D labels).

Published claims (README "Setting 1"): PCK@20 97.25%, PCK@30 98.63%, PCK@40 99.16%, PCK@50 99.48%, MPJPE 0.007 m, 2.23M params, 0.07 GFLOPs.

Measurement (a): their model on their data

Artifact verification (MEASURED, 2026-06-10, this repo eval_repro.py)

Check Result
Parameter count 2,225,042 (2.23M) — matches claim
FLOPs (torch profiler, batch 1) ~0.055 GFLOPs — consistent with 0.07B claim
CPU latency (Windows box, torch 2.12 CPU) 13.2 ms/window @ batch 1 (76/s); 2.48 ms/sample @ batch 64 (403/s)
Checkpoint load weights_only=True (no pickle code execution)

Released checkpoint does NOT reproduce the claims — REFUTED as shipped

Running the released best_pose_model.pth through the released code on the released dataset with the released split procedure (seed-42 file-level 70/15/15; 54,000 test samples) yields:

Metric Published Measured (shipped checkpoint)
PCK@20 97.25% 0.08%
PCK@30 98.63% 0.78%
PCK@40 99.16% 5.53%
PCK@50 99.48% 15.42%
MPJPE 0.007 NaN (dataset contains NaN CSI windows)

Raw output: results/repro_a.json.

Diagnostics (on 2,000 NaN-free windows from the first files of the dataset, i.e. mostly would-be training data — so this is not a split mismatch):

  • Predictions correlate with targets (Pearson r ≈ 0.76) — the checkpoint is a trained model, but in a different keypoint normalization/order than the released data.
  • Best-case post-hoc global per-axis affine correction: PCK@20 ≈ 20%.
  • Best-case per-keypoint affine correction (15×2 fitted transforms — generous cheating): PCK@20 ≈ 72%, still far below 97.25%.
  • Pred↔target keypoint correspondence matrix is degenerate (multiple predicted keypoints best-match the same target joint) — keypoint convention mismatch.

Reproducibility defects in the released artifacts

  1. models/__init__.py imports TemporalConvNet, which models/tcn.py does not define — the published code does not import/run as-is.
  2. The released root checkpoint uses pre-rename module names (att.*, final_conv.*) vs the published code (attention.*, decoder.*) — same shapes/param count, but confirms the checkpoint predates the published code.
  3. The second shipped checkpoint (cross_dataset_test/WiFlow/best_pose_model.pth) is a different architecture (342-channel input = MM-Fi layout, 3 TCN layers, 3-channel/3D decoder) — not usable on their own dataset.
  4. run.py ignores --data_dir and hardcodes ../preprocessed_csi_data.
  5. The released dataset's final 13 files (indices 487499; 9,072 windows, 2.52%) are corrupted: NaN values plus garbage amplitudes up to 3.4e38 (float32 max) in data that is otherwise [0,1]-normalized. Upstream code has no NaN/inf handling; training as published on this download diverges — the first corrupted batch overflows fp16 autocast and permanently poisons BatchNorm running statistics (GradScaler step-skipping does not protect BN). The authors' training curves show normal convergence, so their local data evidently differed from the Kaggle upload. Window masks: results/nan_windows_mask.npy, results/big_windows_mask.npy.

Retraining result (MEASURED, 2026-06-10): claims APPROXIMATELY REPRODUCED

Since the shipped checkpoint is unusable, measurement (a) fell back to retraining with upstream code + defaults (seed 42, batch 64, early-stopped at epoch 41 of 50, best epoch 36, ~75 s/epoch) on ruvultra (RTX 5080). Deviations, all forced and documented: one-line fix for defect (1); torch 2.x+cu128 instead of pinned 2.3.1 (Blackwell sm_120 unsupported); the 9,072 corrupted windows (defect 5) zeroed entirely — without this the published pipeline produces NaN from epoch 1 (observed). Scripts mirrored in remote/; raw metrics in results/eval_retrained.json.

Metric Published Retrained (full test, 54,000) Retrained (corruption-free, 52,560)
PCK@20 97.25% 96.09% 96.61%
PCK@30 98.63% 97.89% 98.23%
PCK@40 99.16% 98.58% 98.79%
PCK@50 99.48% 98.99% 99.11%
MPJPE 0.007 0.0098 0.0094

Within ~0.61.2 PCK points of every published figure (single run, corrupted train windows zeroed, different torch/GPU). Verdict: the accuracy claims are credible and approximately reproducible — but only after repairing the released dataset and code. Val best: PCK@20 96.99%, MPJPE 0.0086 (epoch 36).

One more defect found during the run:

  1. train.py calls plot_training_history, which is not defined anywhere — the built-in post-training test evaluation is unreachable as published (crashes with NameError after training completes).

ADR-152 §2.2 citation rule

Evidence grade for the WiFlow-STD accuracy claims after measurement (a): MEASURED-EQUIVALENT (96.196.6% PCK@20 reproduced by retraining; shipped checkpoint REFUTED; dataset/code require repairs). RuView docs may cite "~96% PCK@20 (our reproduction)" — still not comparable to our 17-keypoint ESP32 numbers (different hardware, 5 subjects, in-domain random split, 15 keypoints).

Edge optimization (measured)

ADR-152 "optimize beyond SOTA" track, 2026-06-10, this Windows box (Windows 11, 16 torch threads, torch 2.12.0+cpu, onnxruntime 1.26.0). Subject: the retrained checkpoint results/retrained_best_pose_model.pth (2,225,042 fp32 params). Scripts: quantize_bench.py, onnx_bench.py, eval_ort_accuracy.py. Raw numbers: results/edge_optimization.json.

Accuracy is on a 10,000-window seed-42 random subset of the corruption-free test split (same seed-42 file-level 70/15/15 split as eval_repro.py; 54,000 test windows, 1,440 corrupted excluded via results/nan_windows_mask.npy | results/big_windows_mask.npy, leaving 52,560; subset drawn with np.random.default_rng(42)). The fp32 subset PCK@20 (96.68%) matches the full clean-test figure (96.61%), so the subset is representative.

Latency is CPU ms/window, median of repeated runs, 3 interleaved repetitions per variant (medians below; run-to-run spread on this box is large, roughly ±20-40% at batch 1 — reps are in the JSON).

Variant Disk size Batch 1 (ms/win) Batch 64 (ms/win) PCK@20 PCK@50 MPJPE
torch fp32 (baseline) 9.07 MB 11.0 2.27 96.68% 99.15% 0.00936
torch fp16 (.half()) 4.58 MB 24.3 2.42 96.68% 99.15% 0.00946
torch int8 dynamic 9.07 MB (unchanged) 15.6 2.06 96.68% (identical) 99.15% 0.00936
ONNX fp32 (onnxruntime) 8.97 MB 3.2 2.0 96.68% 99.15% 0.00936
ONNX int8 (ORT dynamic, supplementary) 2.44 MB 6.5 5.8 96.52% 99.15% 0.01108

Findings:

  • torch dynamic INT8 quantizes nothing on this model. The architecture has zero nn.Linear layers — it is entirely Conv1d (21) + Conv2d (22) + BatchNorm. torch.ao.quantization.quantize_dynamic (requested over {Linear, Conv1d, Conv2d}) converted 0 modules / 0.0% of params: dynamic quantization only has kernels for Linear/RNN-family modules and silently skips convolutions. The "int8" model is bit-identical to fp32 (same outputs, same 9.07 MB). Conv quantization would require static (PTQ) quantization with calibration — out of scope here; the ORT dynamic path below is the honest int8 datapoint.
  • fp16 halves size for free accuracy-wise (PCK@20 0.005 pt, MPJPE +0.0001) but is slower on CPU at batch 1 (~2.2×) — torch CPU fp16 conv kernels are emulated. fp16 is a storage/transport format here, not a CPU runtime win.
  • ONNX Runtime is the real batch-1 latency win: ~3.4× faster than torch (3.2 vs 11.0 ms/window) at identical accuracy (parity 2.4e-7).

Verdict on the paper's "~2.2 MB int8" claim

Plausible but not free, and unreachable by the obvious PyTorch route. 2,225,042 params × 1 byte ≈ 2.2 MB assumes every parameter quantizes. PyTorch dynamic quantization — the one-liner most readers would reach for — yields 9.07 MB (0% quantized) because the model has no Linear layers. ONNX Runtime dynamic quantization, which does have int8 conv weight support, gets 2.44 MB (close to the claim; the overhead is BatchNorm params/buffers and quantization scales kept in fp32) at a measurable accuracy cost: PCK@20 96.68 → 96.52% (0.16 pt) and MPJPE 0.00936 → 0.01108 (+18%), and ~2× slower inference than ONNX fp32 (ConvInteger kernels). The paper does not state a method or an int8 accuracy; treat "2.2 MB" as a weight-arithmetic estimate, achievable in practice only via conv-capable quantization toolchains and with a small accuracy penalty.

ONNX export status

Works. Exported via the TorchScript exporter (dynamo=False), opset 17, with a dynamic batch axis — results/retrained_fp32_dynamic.onnx (8.97 MB), verified to run at batch 1/2/64. The axial attention's view(N*W, C, H) reshape traced correctly (sizes recorded as graph ops, not baked constants). The dynamo exporter also captures the graph but crashed on this box writing a to a cp1252 console (cosmetic Windows encoding issue, not a model blocker). Parity vs torch on the stored fixture (results/parity_fixture.npz, batch 2, seed 42): max abs diff 2.4e-7 — PASS (< 1e-4). ORT-quantized int8 model: results/retrained_int8_ort_dynamic.onnx.

Static PTQ (calibrated) — follow-up

Follow-up to the dynamic-int8 row above (2026-06-10, same box, onnxruntime 1.26.0): ONNX Runtime static post-training quantization (quantize_static, QDQ format, per-channel int8 weights + int8 activations) of the same fp32 export, calibrated on corruption-free TRAINING-split windows only (seed-42 file-level split, same masks; 1,000 windows for MinMax, 512 for the histogram calibrators; never test windows). Scopes: "conv-only" (op_types_to_quantize=["Conv"] — the attention path exports as Einsum/Softmax, which ORT never quantizes anyway, so "all-ops" additionally quantizes the elementwise Mul/Sigmoid/Add/AveragePool glue). Accuracy on the identical 10k-window seed-42 corruption-free test subset; latency median of 3 interleaved reps (fp32/dynamic re-benched in-session as references). Script: static_ptq_bench.py; raw: results/edge_optimization.json (onnx_static_ptq).

Variant Disk size Batch 1 (ms/win) Batch 64 (ms/win) PCK@20 PCK@50 MPJPE
ONNX fp32 (reference) 8.97 MB 2.5 1.9 96.68% 99.15% 0.00936
ORT dynamic int8 (baseline) 2.44 MB 5.7 4.6 96.52% 99.15% 0.01108
static QDQ Percentile(99.99) conv-only 2.53 MB 5.3 4.7 96.61% 99.16% 0.01031
static QDQ MinMax conv-only 2.53 MB 5.2 3.3 96.63% 99.19% 0.01084
static QDQ Entropy conv-only 2.53 MB 5.2 3.1 96.60% 99.19% 0.01078
static QDQ MinMax all-ops 2.60 MB 6.5 3.9 95.45% 99.14% 0.01486
static QDQ Entropy all-ops 2.60 MB 5.7 4.1 95.30% 99.13% 0.01510
static QDQ Percentile all-ops 2.60 MB 5.3 4.3 96.39% 99.17% 0.01218

Verdict: static PTQ (conv-only) is the new best int8 point on accuracy — but only modestly, and it does not fix int8's latency penalty.

  • Accuracy: beats dynamic. All three conv-only calibrations land at PCK@20 96.6096.63% (vs dynamic 96.52%, fp32 96.68% — recovers ~⅔ of the dynamic gap) and MPJPE 0.01030.0108 (vs dynamic 0.01108). Best MPJPE: Percentile conv-only, +10% over fp32 instead of dynamic's +18%.
  • Size: slightly worse. 2.53 MB vs 2.44 MB (+3.6%) — QDQ nodes and per-channel scales cost a little; BatchNorm stays fp32 in both (the 12 BNs follow Slice/Einsum/Reshape, never Conv, so they cannot be folded).
  • Latency: a wash vs dynamic, still ~2× slower than ONNX fp32 at batch 1. Batch-1 medians 5.25.3 vs dynamic 5.7 ms/win in-session — within this box's ±2040% noise. Batch 64 leans static (3.13.3 for MinMax/Entropy conv-only vs 4.6), same caveat.
  • All-ops QDQ is strictly worse: up to 1.4 pt PCK@20 and +60% MPJPE for zero size/latency benefit — int8 activations through the elementwise glue around the attention blocks is where the damage is. Conv-only is the right scope.
  • Negative result worth recording: Entropy calibration is a no-op here — on an identical calibration set it selects full-range thresholds bit-identical to MinMax (all 247 scales equal; verified on a 64-window smoke set). Also, ORT 1.26's CalibMaxIntermediateOutputs raises a spurious "No data is collected" when the batch count divides the chunk size (worked around in the script).

Deployment guidance: need speed → ONNX fp32 (3.2 ms b1). Need int8 weights for size → static QDQ conv-only (Percentile or MinMax, results/retrained_int8_static_percentile_conv.onnx), which strictly dominates dynamic int8 on accuracy at ~equal latency and +0.09 MB.

Measurement (b): BLOCKED-ON-DATA (attempted 2026-06-10)

The fine-tune-on-ESP32 measurement stopped at dataset characterization, per the pre-registered stop rule (<2,000 paired windows). Findings (MEASURED):

  • Only one trainable paired dataset exists: ruvultra:~/work/cog-pose-train/paired.jsonl — 1,077 windows (one subject, one room, one 29.9-min session, single node; CSI [56, 20]; 17 COCO keypoints, MediaPipe confidence mean 0.44 — only 264 windows pass ADR-079's own conf>0.5 training filter). Prior measured attempts on this exact set: 03% torso-PCK@20 (temporal splits, three independent pipelines). Fine-tuning a 2.23M-param model on ~860 train windows would measure memorization, not transfer.
  • The April session behind the old "92.9% PCK@20" claim is lost (345 samples, 35 subcarriers; raw CSI gone from ruvzen/ruvultra/cognitum-v0; only a 69-sample predictions+GT holdout survives at models/wiflow-real/eval-holdout.jsonl).
  • Forensic recheck of that holdout RETRACTS the 92.9% figure: the trainer's pck() used an absolute 0.2 image-unit threshold (not torso-normalized) and the model output a constant pose (pred std 0.0000 across 69 near-static frames; a mean predictor scores 100% under the same protocol). The torso-normalized PCK@20 on the same holdout is 19.1%. This corroborates the 2026-05-11 audit retraction (CHANGELOG, PR #535); stale doc citations were removed 2026-06-10 (user-guide, readme-details, ADR-152 §2.1.3). The §2.2 no-citation rule now applies to ADR-079 accuracy claims.

Unblock criteria: a paired collection session of ≥2k windows (≈35+ min at the observed stride; multi-pose, conf>0.5, ideally with the §2.1.3 two-checkerboard calibration), plus a re-baselined our-pipeline number under torso-PCK@20 on the same split. WiFlow-STD assets stand ready on ruvultra (~/wiflow-std-bench/). Also worth investigating: ADR-079's protocol predicts ~9k windows per 30 min; the May session under-delivered ~8× (aligner drop rate?).

Pending

  • (b) fine-tune on our ESP32 17-keypoint eval set — BLOCKED-ON-DATA, see above.
  • (c) our internal WiFlow on their dataset (15-keypoint subset mapping) — also affected: there is currently no validated internal pose model to compare (the 92.9% artifact is retracted; the MM-Fi SOTA models in ADR-150 §3 are a different input domain).