MoT boundary fixes, hybrid configs, and drift-immune latency benchmarking - #233
Open
BochunYuan wants to merge 10 commits into
Open
MoT boundary fixes, hybrid configs, and drift-immune latency benchmarking#233BochunYuan wants to merge 10 commits into
BochunYuan wants to merge 10 commits into
Conversation
Found while completing the boundary-test suite in tests/test_mot.py. Each was reachable from a plain model YAML, and none raised at construction time -- they surfaced as a crash or a silently degraded model much later. 1. window_size <= 0 -> ZeroDivisionError deep inside the window expert (H % win). Now rejected in both MoTBlock and _WindowTransformerExpert with a message naming the offending value. 2. n_points = 0 silently zeroed the entire deformable attention branch: the weighted sum over sampling points is empty, so the expert degenerated to its FFN while still reporting as a deformable expert. Now rejected -- a routed expert that quietly stops attending is worse than a startup error. 3. shift_size was coerced through `bool`, so any truthy integer became win//2. An explicit shift is now honoured as a value and wrapped into [0, win); `True` keeps the Swin default win//2. Values >= win would realign the windows and are folded by the modulo rather than silently disabling the shift. 4. Shifted windows had no attention mask. The cyclic roll brings opposite image edges into the same window, so without a mask those spatially distant tokens attended each other -- information leaking across the feature-map border, which canonical Swin explicitly masks. _shift_attn_mask labels groups in the rolled frame so band edges land on the window grid and fully-interior windows stay unmasked. The measured effect of (4) on VisDrone mAP50-95 is -0.0089%, ~200x smaller than the run-to-run jitter band, so published comparisons are unaffected; the fix is for correctness, not for the numbers. Two supporting changes: - _sdpa now converts a boolean mask to 0/-inf before the pre-2.0 fallbacks, which add the mask rather than interpreting attend flags. Without this, (4) would be correct only on PyTorch >= 2.0. - Expert outputs are cast to the accumulator dtype before blending, so an expert returning a different precision under AMP cannot silently upcast or error out of the weighted sum. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rom YAML
The per-block shift policy was hardcoded as `bool(i % 2)` inside the
C2fMoT constructor with no way to override it, which made a shift-strategy
ablation impossible to express in a model YAML -- the one experiment the
Swin-style alternation invites.
`window_shift` now accepts:
"alternate" (default) -- even blocks regular, odd blocks shifted.
Byte-identical to the previous behaviour, so
every existing YAML and checkpoint is unaffected.
True / False -- force one policy onto every block, which is what
the ablation needs.
Anything else raises ValueError rather than being silently coerced. The
flags are resolved once at build time, keeping inference deterministic and
tracing stable for export.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the three cases that were specified plus paired regression tests for each defect fixed in the preceding commits. Specified cases: - window_size larger than the feature map must degrade, not crash. It already did (the expert clamps to min(H, W)); the test pins that down. - _WindowTransformerExpert's shift on odd-sized inputs. Also already correct via pad-then-crop; now covered for 1xN, Nx1, and odd HxW. - exploration_eps must be disabled in eval mode. Verified as a true negative: routing is identical across repeated eval forwards and the noise term is gated on self.training. Tested rather than assumed, because "already correct" was a claim about code I had not run. Regression tests for the fixed defects: window_size <= 0 and n_points < 1 now raise; an explicit integer shift_size survives instead of collapsing to win//2; and shifted windows no longer attend across the image border (asserted by driving a single hot pixel at one edge and checking the opposite edge stays cold). window_shift is covered twice -- once at the constructor and once end to end through DetectionModel from a YAML arg list, since the constructor accepting a parameter that tasks.py never forwards would leave the ablation just as unreachable as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Config-only, per the project convention that architecture variants come
from YAML rather than code. All four build at nc=80 and are covered by
the unified 8-arm benchmark.
hybrid-h2 : MoT on P4 + MoA on P5 3.896M 12.20 GFLOPs
hybrid-h3 : MoT on P4 only (2 blocks) 3.759M 10.61 GFLOPs
hybrid-h4 : MoA on P4 + MoT on P5 3.748M 10.69 GFLOPs
mot-backbone : MoT replaces the P4/P5 backbone MoE junctions, dense
neck identical to the baseline
3.202M 9.78 GFLOPs
None of these beats the MoE baseline on VisDrone (100 epochs, seed 42):
best is h2 at +1.11% mAP50-95 on the final epoch, but that falls under the
+1% synergy threshold on both the best-epoch and last-20-epoch readings,
so it reads as a tie rather than a gain. A multi-seed re-check is running.
They are added because the ablation is only reproducible with them, not
as recommended configurations.
Two results worth recording for anyone extending this:
- mot-backbone is the only routed arm with baseline-parity tail latency
(P99 19.83 vs 19.86 ms) because it *replaces* rather than stacks routing
blocks. Its mAP is the lowest of the eight arms (-2.59%).
- It also shows that the ES-MoE placement rule does not transfer to MoT:
for MoT, neck-sited beats backbone-sited, the opposite of the published
MoE ordering, and removing the backbone/neck cascade did not recover the
deficit.
mot-backbone keeps MoE on P3 deliberately: the LocalConv expert attends
over all H*W tokens, so P3/8 at 640x640 is 6400 tokens and OOMs a 24GB
card (15.5 GiB single allocation). P4/P5 are 16x/100x cheaper. Putting MoT
on P3 needs a windowed or linear-attention expert first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mune benchmark_row measures each model in one contiguous block, so any drift in GPU state during that block -- vGPU contention, clock throttling -- is charged entirely to whichever arm happened to hold the GPU. On this host that was enough to flip conclusions between sessions: the baseline measured p50 16.87 ms in one run and 11.66 ms in another, and the h3 arm came out both faster and slower than the pure-MoT arm depending on run order. --interleave keeps every model resident and times one forward per arm per cycle, rotating the visit order so no arm sits at a fixed position. Drift then lands on all arms alike and cross-arm differences stay valid even when absolute numbers move. Re-measuring the 8 arms over 300 cycles gave a first-half/second-half p50 spread of <= 0.11 ms per arm with a stable ordering, against the ~5 ms inter-session disagreement before. Rows now carry a "sampling" field, and interleaved rows are never shadowed by contiguous ones when merging into an existing CSV -- otherwise a later contiguous run would silently overwrite the trustworthy numbers. Scope: interleaving is correct for comparing arms. It cannot measure within-arm jitter, since each arm's samples are spread across the whole session; that needs per-arm contiguous blocks with block-level interleaving, which this flag does not do. Also registers the four new variants (v10_h2/h3/h4, v10_mot_bb) so the suite can train and benchmark them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zeropower_via_newtonschulz5 asserts a 2-D input, and muon_update only reshaped the 4-D conv case. Any model whose parameters include a 3-D tensor therefore killed MuSGD with a bare AssertionError. MoT and MoA experts introduce exactly that (12 three-dimensional parameters in the MoT-N variant against 0 in the baseline), so the optimizer was unusable on the routed architectures. 3-D updates are now flattened to (size(0), -1), matching how the 4-D conv case is handled. Sub-2-D updates (1-D biases and norms, 0-D scalars) return unchanged, since there is no subspace to orthogonalize; trainer.py only routes ndim >= 2 params into the Muon group, so that branch guards hand-built param groups rather than a live training path. Deliberately narrow: an earlier version of this patch also short-circuited matrices with a singleton dimension, which silently changed the update for every 1xN and Nx1 parameter (34 of them in the baseline model) without a crash to justify it. That is now left alone -- every shape that worked before produces bit-identical output, and only the two crashing shapes change behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Instruments MoTBlock routers to answer which of the three Transformer experts (LocalConv / Window / Deformable) a token is dispatched to, and whether that pattern shifts with scene content. mot_routing_interpret.py hooks + per-image routing records run_mot_routing_interpret.py driver: checkpoint -> records -> stats mot_routing_figures.py expert-activation heatmaps and plots Statistics deliberately go beyond per-scene means, because the first pass produced a finding that did not survive scrutiny: "occlusion suppresses the Deformable expert" had p=0.002, a consistent sign across arms, and a plausible mechanism -- but occluded images average 74.7 objects against 47.7 for unoccluded ones, and the effect vanished (p=0.93) once the test was redone within the dense stratum. The tooling therefore includes stratified permutation tests and BH FDR correction over the full family of comparisons, so a density confound cannot pass as a routing result. Two test modules ship with it: test_mot_routing_scene_contrasts.py covers the contrast and stratification maths, and test_mot_ablation_summary.py pins the loss-column contract in the comparison harness (the trainer emits a fused train/mixture_aux_loss column, not the legacy per-mixture names, so a rename there would silently drop the aux term from every total). Paths come from CLI flags; the absolute paths in the docstrings are usage examples from this host, not defaults. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MoT boundary fixes, YAML-reachable shift policy, and drift-immune latency benchmarking
Branch:
feat/mot-boundary-fixes-and-hybrid-configs(7 commits, based onmain@ 296a29c)What this changes
Five defect fixes in the MoT stack, the boundary-test suite that found them, four
config-only architecture variants, and a correction to how the ablation harness
measures latency.
Nothing here changes the behaviour of an existing YAML or checkpoint. The one
change that touches numerics at all (the Swin shift mask) moves VisDrone mAP50-95
by −0.0089%, roughly 200× smaller than the run-to-run jitter band.
Defects fixed
Each was reachable from a plain model YAML, and none raised at construction time —
they surfaced as a crash or a silently degraded model much later.
window_size <= 0unvalidatedZeroDivisionErrordeep inside the window expertn_points = 0unvalidatedshift_sizecoerced throughboolwin//2; an explicit shift was unreachablemuon_updateonly reshaped 4-D gradszeropower_via_newtonschulz5asserts 2-D, so MuSGD died on any model with 3-D params. MoT-N has 12; the baseline has 0 — the optimizer was unusable on the routed architecturesTwo supporting changes:
_sdpanow converts a boolean mask to0/-infbefore thepre-2.0 fallbacks (which add the mask), without which fix 4 would only be correct
on PyTorch ≥ 2.0; and expert outputs are cast to the accumulator dtype before
blending, so an expert returning a different precision under AMP cannot silently
upcast or error out of the weighted sum.
window_shiftis now reachable from YAMLThe per-block shift policy was hardcoded as
bool(i % 2)inside theC2fMoTconstructor, which made a shift-strategy ablation impossible to express in a model
YAML — the one experiment the Swin-style alternation invites.
"alternate"(the default) is byte-identical to the previous behaviour;true/falseforce one policy onto every block. Anything else raises
ValueErrorinstead of beingsilently coerced. Flags resolve once at build time, so inference stays deterministic
and tracing stays export-stable.
Covered twice — at the constructor and end-to-end through
DetectionModel— since aconstructor accepting a parameter that
tasks.pynever forwards would leave theablation just as unreachable as before.
Latency measurement was wrong for cross-arm comparison
benchmark_rowmeasures each model in one contiguous block, so drift in GPU stateduring that block — vGPU contention, clock throttling — is charged entirely to
whichever arm happened to hold the GPU. On our host that flipped conclusions between
sessions: the baseline measured p50 16.87 ms in one run and 11.66 ms in
another, and the
h3arm came out both faster and slower than pure MoT dependingon run order.
--interleavekeeps every model resident and times one forward per arm per cycle,rotating visit order so no arm sits at a fixed position. Re-measuring 8 arms over 300
cycles gave a first-half/second-half p50 spread of ≤ 0.11 ms per arm with stable
ordering, against the ~5 ms inter-session disagreement before.
Rows carry a
samplingfield, and interleaved rows are never shadowed by contiguousones when merging into an existing CSV — otherwise a later contiguous run would
silently overwrite the trustworthy numbers.
Scope limit, stated because it bit us: interleaving is correct for comparing
arms. It cannot measure within-arm jitter, since each arm's samples are spread
across the session. That needs per-arm contiguous blocks with block-level
interleaving, which this flag does not do. We had published a tail-latency-stability
claim that neither reading supports; it has been retracted.
New configs (config-only, per project convention)
yolo-master-nhybrid-h2hybrid-h3hybrid-h4mot-backboneThese are added for reproducibility, not as recommendations. None beats the MoE
baseline on VisDrone (100 epochs, seed 42). The best,
h2, reaches +1.11% mAP50-95 onthe final epoch but falls under the +1% threshold on both the best-epoch and
last-20-epoch readings — three readings straddling the line means a tie, not a gain.
The multi-seed re-check has since finished (seeds 42/43/44, both arms, 100 epochs each)
and the +1.11% did not reproduce: pooled over n=3 the deltas are −1.58% (final),
−1.48% (best) and −1.42% (last-20), and the per-seed final deltas are +1.11% / −3.34% /
−2.46% — seed 42 was the only positive one. The paired 95% CI is [−0.0128, +0.0073],
which includes zero, so
h2is statistically indistinguishable from the baselinerather than worse. The underlying problem is that the criterion cannot resolve the
effect: the +1% threshold is 0.00172 mAP against a between-seed sd of 0.00291 (0.59×),
and the baseline's own three seeds span 0.00262 — 1.5× the threshold. Re-running the
baseline under a different seed can manufacture a ">1%" effect on its own.
Two results worth recording for anyone extending this:
mot-backboneis the only routed arm with baseline-parity tail latency(P99 19.83 vs 19.86 ms) because it replaces rather than stacks routing blocks:
3 MoE junctions become 1 MoE + 2 MoT, with no net increase. Its mAP is the lowest of
the eight arms (−2.59%).
backbone-only placement ahead of neck-only; for MoT the ordering is reversed, and
removing the backbone/neck cascade did not recover the deficit — so cascading was
not the cause. Caveat: this arm replaces the P4/P5 MoE rather than adding to it
(0.248 M fewer params than baseline), so the honest reading is "at equal parameter
budget, swapping backbone MoE for MoT loses", not "MoT fails in the backbone".
mot-backbonekeeps MoE on P3 deliberately: the LocalConv expert attends over allH·W tokens, so P3/8 at 640×640 is 6400 tokens and OOMs a 24 GB card (15.5 GiB single
allocation). P4/P5 are 16×/100× cheaper. Putting MoT on P3 needs a windowed or
linear-attention expert first.
Routing interpretability tooling
mot_routing_interpret.py,run_mot_routing_interpret.py, andmot_routing_figures.pyinstrument the routers to answer which expert a token isdispatched to and whether that shifts with scene content.
The statistics deliberately go beyond per-scene means, because the first pass produced
a finding that did not survive scrutiny: "occlusion suppresses the Deformable expert"
had p=0.002, a consistent sign across arms, and a plausible mechanism — but occluded
images average 74.7 objects against 47.7 for unoccluded ones, and the effect vanished
(p=0.93) once the test was redone within the dense stratum. The tooling therefore
includes stratified permutation tests and BH FDR correction over the full comparison
family, so a density confound cannot pass as a routing result.
Testing
ruff checkandruff format --checkare clean on every line this branch adds. Thepre-existing violations in the touched files are left alone rather than swept into a
functional PR.
The three specified boundary cases are all covered. Two of them turned out to be true
negatives —
window_sizelarger than the feature map already degraded correctly (theexpert clamps to
min(H, W)), andexploration_epswas already gated onself.training. They are tested rather than assumed, because "already correct" was aclaim about code that had not been run.
Not included
MoT_MoA_Ablation_Report.mdand three host-specific helper scripts(
collect_final_results.py,convert_visdrone.py,monitor_training.sh,run_mot_interpretability.py) are left untracked. They hardcode absolute paths forthis machine. The report in particular is a superseded COCO128 run whose mAP values
are ~0.0002 and whose ordering does not reproduce on either real dataset.