# 🎯 0.1.0 — Detection

## What was reproduced

The detector as [R1] describes it: a DFL-free dual-head architecture whose one-to-one branch supports NMS-free inference, trained with Small-Target-Aware label assignment, Progressive Loss branch reweighting, and the MuSGD hybrid optimizer.

| mechanism | [R1] reference | implementation |
| -- | -- | -- |
| dual head, DFL-free | sec. 3.2, Fig. S2 | `models/heads/detect.py` — one-to-many and one-to-one branches share a backbone/neck forward; boxes are direct ltrb distances, no distribution bins |
| NMS-free E2E decode | sec. 3.2.2 | `decode/topk_e2e.py` — top-k over the one-to-one branch, no suppression op |
| TAL assignment | R4 (arXiv:2108.07755) | `assign/tal.py` — `t = s^alpha * u^beta`, alpha 1, beta 6, per-GT target normalization |
| STAL small-target assignment | sec. 3.3.3 | `assign/stal.py` — size-aware surrogate boxes widen the candidate set for small ground truths |
| Progressive Loss | sec. 3.3.1 | `losses/progressive.py` — branch weight ramps `alpha_init 0.8 -> alpha_final 0.1` as a fraction of the epoch budget |
| MuSGD | sec. 3.5 | `optim/musgd.py` + `optim/newton_schulz.py` — Muon/SGD hybrid at `w_muon 0.5 / w_sgd 0.5` |

Architectural fidelity is gated rather than asserted: `test_param_flops.py` holds all five scales within ±2% params and ±5% FLOPs of [R1, Table 7] (n: 2.4M/5.4G … x: 55.7M/193.9G).

## Det-smoke acceptance

The Det-smoke tier (blueprint sec. 10 tier A): n-scale, ~50 COCO epochs, ~1 GPU-day.

**Run v8** — dev12, COCO 2017 train, batch 128, lr 0.02, MuSGD, warmup 3 epochs then linear decay to `lr0 * 0.01`, close-mosaic for the final 10, EMA (decay 0.9999, tau 2000), 16-mixed, seed 0, 50 epochs = 46,250 steps on one RTX PRO 6000, ~8m45s/epoch.

Evaluated on the full COCO val2017 (5000 images) with `scripts/eval_det.py`, which runs one forward per batch and decodes both paths from it, scoring with `torchmetrics` `MeanAveragePrecision` on the `faster_coco_eval` backend:


| weights | path | mAP50-95 | mAP50 | mAP75 | mAP_S | mAP_M | mAP_L | mAR_100 |
| -- | -- | -- | -- | -- | -- | -- | -- | -- |
| EMA | NMS | **25.30** | 38.12 | 26.94 | 11.86 | 27.09 | 33.88 | — |
| EMA | E2E | 23.84 | 36.00 | 25.15 | 11.64 | 25.86 | 31.38 | 46.07 |
| raw | NMS | 25.33 | 38.45 | 27.05 | — | — | — | — |
| raw | E2E | 24.23 | 36.72 | 25.82 | 11.24 | — | — | — |


| criterion | required | observed |  |
| -- | -- | -- | -- |
| stable training | no divergence | monotonic validation descent, 50/50 epochs | ✓ |
| val mAP50-95 | > 25 | **25.30** EMA-NMS, 25.33 raw-NMS | ✓ |
| E2E within NMS path | ~1.5 AP | 1.46 EMA, **1.11 raw** | ✓ |
| Phase ≤5 gates | green | 474 tests, 5/5 goldens | ✓ |

**The E2E deficit is itself a reproducible claim.** [R1 sec. 4.4] reports 0.6–0.8 AP between the NMS-free and NMS paths. This run measures 1.11 AP on raw weights and 1.46 on EMA — the right sign and the right order of magnitude, wider than the paper's figure. Whether the residual gap closes at longer schedules is a Det-ablations question, recorded here rather than explained away.

![Det-smoke training curves](https://borda.github.io/lucid-YOLO/figures/det_smoke_training.svg)

*Run v8. Left: the epoch-end E2E mAP proxy logged during training (letterbox coordinates — the acceptance figure above comes from the standalone evaluator). Middle: epoch-mean totals, log axis. Right: the one-to-one branch's pre-gain components; the L1 term is in stride units.*

## What went wrong first, and why it matters

Three earlier attempts failed the same criterion, and the failure is more instructive than the pass.

| run | epochs | mAP50-95 (EMA NMS) | mAR_100 |
| -- | -- | -- | -- |
| v2 — attempt 1 | 44/50 | 3.96 | 16.6 |
| v4 — attempt 2 | 100 | 6.28 | 23.7 |
| v7 — stale wheel | 100 | 4.00 | — |
| **v8 — attempt 3** | **50** | **25.30** | **46.07** |

Attempt 2's curves carry the signature plainly: the total loss descends the whole way while the components sit flat — its validation L1 term never moves from ~39 between epochs 30 and 90, and validation classification *worsens* from 6.88 to 7.31 while the training figure falls. A loss going down while nothing underneath it moves is what a mis-scaled term looks like.

**Root cause 1 — coordinate frame of the L1 term (A13, WP-078).** The head predicts ltrb distances in *stride units*; the module fed the loss *pixel* boxes. The legacy DFL-field gain (6.0) is calibrated for the stride frame, so the term ran 8–32× large. Measured on the attempt-2 endpoint, the one-to-one objective decomposed as **97.1% L1, 1.7% classification, 1.1% CIoU** — and the gain-weighted arithmetic reproduces the logged validation total exactly, which is what turned a hypothesis into a diagnosis.

The consequence was visible in the classifier, not the boxes. Probed on the pre-fix checkpoint: predicted probability **0.076** at the assigned positive's own class, only **8 of 67,200** anchors above 0.25 confidence, and a mean per-anchor confidence *below* the 0.01 prior the head was initialized to (A30). Boxes were tight; the scores that rank them were noise. mAP is a ranking metric, so the runs sat in single digits regardless of localization quality.

Dividing the L1 differences by each positive anchor's stride moved classification from 1.7% to **23.0%** of the objective.

**Root cause 2 — augmentation RNG collapse ([WP-079](https://borda.github.io/lucid-YOLO/work_packages/wp-079/)).** `_TrainPipeline` seeds one generator at construction, in the parent process, and that copy never advances because the parent never calls `__getitem__` when workers are used. Every worker therefore replayed one identical augmentation-parameter stream (diversity `1/num_workers`), and once workers became non-persistent for unrelated memory reasons ([WP-076](https://borda.github.io/lucid-YOLO/work_packages/wp-076/)) they restarted from that same state every epoch. At 32 workers × 100 epochs the run drew roughly `1/3200` of the intended augmentation parameters. Re-seeding each worker from `WorkerInfo.seed` restores per-worker, per-epoch streams while keeping the run reproducible from the datamodule seed.

**What this cost, and the lesson taken.** Both defects are semantic — nothing crashed, no test failed, every gate stayed green through three failed runs totalling ~35 GPU-hours. Unit tests that check shapes and finiteness cannot see a term that is the wrong size or a random stream that repeats. The regression net for 0.2.0 onward therefore pins *invariants* (loss-term balance, augmentation stream divergence) rather than only shapes, so that a semantic break fails in minutes instead of surfacing as a disappointing mAP a day later.

## Assumption outcomes

Assumptions the detector tier exercised. Full register in `ASSUMPTIONS.md`.

| id | subject | outcome |
| -- | -- | -- |
| A8 | LR schedule shape | **validated** — warmup 3 epochs + linear decay to `lr0*lrf` carries the acceptance run; the constant-LR deferral it replaced plateaued at 3.96 |
| A13 | loss gains and L1 coordinate frame | **revised, then validated** — gains 7.5/0.5/6.0 from [R1 Table S2] kept; frame corrected from pixels to stride units |
| A30 | classification bias prior init | **validated** — `-log((1-pi)/pi)`, pi 0.01, per R24 sec. 5.1; without it the first MuSGD step killed the network |
| A2 | TAL exponents | held at alpha 1, beta 6; not independently ablated at A-tier |
| A31 | warmup convention | absorbed into A8 |
| A32 | train-time resampling filter | non-antialiased fused warp; no acceptance-visible effect |
| A33 | uint8 batch transport | validated — no acceptance-visible effect, 4× smaller IPC |

## Deviations from the paper

1. **Epoch budget.** ~50 epochs at n-scale, against the paper's from-scratch 500/600-epoch schedules. Det-smoke is a smoke tier by design; the headline comparison against [R1 Table 4] is Det-C and remains a stretch goal.
2. **No Objects365 pretraining, no evolutionary hyperparameter search** (D2).
3. **Batch-scaled learning rate.** lr 0.02 at batch 128, linear scaling from the batch-64 recipe. The unscaled lr 0.01 run at the same batch is attempt 2's configuration and is not separable from the loss-frame defect it also carried, so this project has no clean measurement of the scaling on its own.
4. **E2E deficit wider than published** — 1.11 AP raw against 0.6–0.8 in [R1 sec. 4.4]; see above.

## Reproducing this result

```bash
pip install lucid-yolo==0.0.1.dev12
lucid-download --data-root <root> --splits train val --verify
lucid-yolo fit --config det_smoke.yaml \
  --data.data_root <root> --data.batch_size 128 --data.num_workers 32 \
  --data.prefetch_factor 1 --model.lr 0.02 --trainer.max_epochs 50 \
  --trainer.precision 16-mixed
python scripts/eval_det.py <checkpoint> --data-root <root>
```

These are the commands **as run**, at the version named, and are left unedited for that reason. On 0.3.0 and later the same three steps are `lucid-data download --data_root <root> --splits '[train,val]' --verify true`, the unchanged `lucid-yolo fit`, and `lucid-eval --checkpoint <checkpoint> --data_root <root>` ([WP-096](https://borda.github.io/lucid-YOLO/work_packages/wp-096/)). `lucid-download` kept accepting the dashed flags above through 0.3.0; it was removed in 0.4.0 as that release said it would be ([WP-110](https://borda.github.io/lucid-YOLO/work_packages/wp-110/)), so on 0.4.0 and later the block above must be read through those replacements rather than run as written.

Seed 0 throughout. Cross-platform bitwise reproduction is not claimed (A26: libm last-bit rounding differs across OS and architecture); the run config, seeds, and metric reports are archived under `.experiments/det_smoke/`.
