# Диагностика: коллапс recall на эпохе 1 — DSS + MoCo queue

**Дата:** 2026-04-25
**Конфиг:** `conf/gtauav_balanced.gin` (gate=0.7, with text)
**Запуск:** `python -m src.training.train_gtauav --config conf/gtauav_balanced.gin --filter-meta meta/seg_filter.json`

---

## 1. Симптомы из лога

### Метрики по эпохам

| Эпоха | Режим сэмплера | LR | train loss | train R@1 | val R@1 | val AP | eval loss |
|---|---|---|---|---|---|---|---|
| **ep0** | mutex-only (warmup) | 5.00e-05 | 4.2239 | **0.0742** | 0.0758 | 0.1511 | 2.0783 |
| **ep1** | DSS активирован, MoCo queue полная | 1.00e-04 | 4.4218 ↑ | **0.0120** ↓6× | 0.0107 | 0.0328 | 2.0786 |

### Ключевые наблюдения
- **Recall обвалился ровно на эпохе включения DSS** (warmup=1, поэтому DSS стартует на ep1)
- **Train loss выросла** (4.22 → 4.42), хотя должна падать
- **Eval loss практически стоит** (2.0783 → 2.0786) — модель не учится на чистом батч-сигнале
- **Разрыв train/eval loss > 2×** — большая часть train loss идёт от MoCo+hard-mining компонент, не от in-batch contrast
- Gate stable: `gate_q=0.70`, `gate_g=0.70` (текст не вытаскивает дополнительный градиент)
- `tau=0.07` (clamped, learnable, но почти не двигается → loss не учит температуру)

### Сбой при сохранении чекпойнта
```
RuntimeError: [enforce fail at inline_container.cc:668] .
unexpected pos 289048128 vs 289048016
```
**Причина:** диск переполнен (ENOSPC). Подтверждено независимо: `mkdir` в session-env тоже падает с ENOSPC. Это инфраструктурный баг, не алгоритмический — но он не даёт сохранить состояние и продолжить.

---

## 2. Почему DSS «не работает» на этом этапе

### 2.1. Mutex-constraint спасает от false negatives только формально
Mutex исключает дронов с пересекающимися `sat_candidates`. Но DSS специально пакует **визуально похожих** дронов — у которых sat-кандидаты могут быть *разными tiles*, но при этом сцены практически идентичные:
- один и тот же район Лос-Сантоса
- та же высота полёта
- та же time-of-day, та же погода (синтетика GTA-V)

Для холодного энкодера такие «negatives» неотличимы от позитива → softmax пытается развести то, что развести нечем → градиенты шумные и противоречивые.

### 2.2. Re-embed раз в эпоху — слишком редко в начале обучения
- На ep1 LR удваивается (5e-5 → 1e-4 после warmup)
- Encoder за эпоху смещается сильно (MONA-адаптеры учатся быстро)
- Батчи внутри эпохи собираются по embeddings, посчитанным **до** этих обновлений
- То есть «similarity» в DSS — это similarity **вчерашнего** энкодера
- Re-embed на ep1 занял 313s, но между двумя re-embed encoder успевает измениться существенно

### 2.3. Эпохи 0 на mutex-only недостаточно как warmup
- К моменту запуска DSS R@1=0.074 на полной gallery (~2684 уникальных тайлов)
- Random baseline ≈ 1/2684 ≈ 0.0004
- 0.074 — это «модель чуть-чуть оторвалась от случая», но качество embeddings ещё недостаточно
- DSS усиливает шум, а не сигнал: «визуально похожих» определяет *шумный* энкодер

---

## 3. Почему MoCo queue делает хуже

### 3.1. Representation drift в queue
- Queue хранит **4096 embeddings**, посчитанных в разные моменты времени разными версиями encoder
- На ep0–1, когда MONA-адаптеры учатся быстро, разница между «свежим» и «3 шага назад» embedding'ом — заметная
- Эти устаревшие негативы дают сигнал в направлении, в котором энкодер уже не находится
- Без momentum encoder (как в оригинальном MoCo) drift ничем не сглажен

### 3.2. `hard_mining_k=512` амплифицирует ошибку queue
- Из 4096 берутся 512 «самых трудных»
- «Трудность» меряется в **текущем** feature space
- Сами вектора лежали там в **старом** feature space
- На холодном энкодере это эквивалентно «учиться отличать себя-позитива от себя-вчерашнего»
- Loss растёт, recall падает

### 3.3. Тройная hard-negative композиция → collapse-режим
Все три механизма независимо подсовывают «трудные» негативы:
| Механизм | Источник трудности |
|---|---|
| DSS | визуально близкие в батче |
| Mutex sampler | in-batch contrast после фильтра |
| `hard_mining_k=512` | top-K из MoCo queue |

На warm encoder это polish; на cold encoder это **too-hard negatives problem** (известная failure mode у contrastive learning, Robinson et al., 2021):
- Модель не получает «лёгких» примеров, на которых формируется базовое embedding-пространство
- Градиенты толкают её в произвольных направлениях
- Embedding space коллапсирует или размывается

---

## 4. Подтверждения из истории коммитов

```
8f8cbb1 Diagnostic baseline v2: also disable MoCo queue
c25bd64 Diagnostic baseline: disable DSS + hard mining, fresh output dir
9a7fbff Fix plot_combined: fallback from 'total' to 'train_loss'
70f1617 Fix autograd in-place error: move memory-bank enqueue after backward
8197ab2 Fix training loop: only pass positive_weights to WeightedInfoNCELoss
```

Подозрение на DSS+MoCo уже было — текущие числа подтверждают его эмпирически.

---

## 5. Рекомендации

### 5.1. Cold-start curriculum (приоритет)
1. **Несколько эпох mutex-only без MoCo и без hard_mining** до R@1 хотя бы ~0.2 на train.
   - Конкретно: `sampler_type="mutex"`, `loss.use_memory_bank=False`, `loss.hard_mining_k=0`.
2. Затем **включить MoCo queue с warm-up**: либо momentum encoder, либо queue pre-fill 1 эпоху со `stop_grad` на queue updates.
3. **DSS включать только после** того, как embeddings стали discriminative (R@1 на train > 0.2). Иначе «similar» = «random».
4. **`hard_mining_k` стартует с 0** и поднимается curriculum-схемой (например, 0 → 64 → 256 → 512 по эпохам).

### 5.2. Изменения по DSS
- Сократить интервал re-embed (раз в N шагов, не раз в эпоху) — минимум первые 2-3 эпохи
- Или временно фиксировать кеш embeddings из чистого baseline (без DSS) и использовать его как «референсный» для сэмплинга

### 5.3. Изменения по MoCo
- Добавить momentum encoder (EMA на ключи, как в оригинальном MoCo) — это решает drift
- Либо очищать queue на каждой эпохе (теряем эффект, но избегаем drift)
- Размер queue 4096 при batch 64 → 64 батча в очереди = слишком долгая история для холодного энкодера

### 5.4. Инфраструктура
- **Освободить диск** (ENOSPC блокирует чекпойнты и часть утилит)
- Возможно, перенести `out/gtauav/` на другой диск или почистить старые runs
- Добавить pre-flight disk check перед `torch.save`

---

## 6. План эксперимента для подтверждения

| Run | Sampler | MoCo queue | hard_mining_k | Цель |
|---|---|---|---|---|
| **A** (clean baseline) | mutex | off | 0 | Подтвердить, что без DSS/MoCo recall растёт нормально |
| **B** (only MoCo) | mutex | on | 0 | Изолировать вклад queue |
| **C** (only hard_mining) | mutex | on | 512 | Изолировать вклад top-K mining |
| **D** (only DSS) | dss | off | 0 | Изолировать вклад DSS |
| **E** (full, current) | dss | on | 512 | Reproduce coллапс |

Decision rule:
- Если A учится нормально (R@1 растёт монотонно), а E коллапсирует — это decisive ablation
- Если B/C/D по отдельности тоже коллапсируют — проблема в каждом компоненте
- Если только E коллапсирует — проблема в композиции

---

## 7. Альтернативная гипотеза (менее вероятная)

LR=1e-4 на проекциях после warmup может быть слишком большим для shared DINOv3 + MONA с малым trainable %. Симптомы похожи (резкий обвал на эпохе с полным LR), но не объясняют, почему mutex-only baseline (commit `c25bd64`) учится нормально без изменения LR. Так что это вторичный фактор.

---

## 8. Резюме

**Главный диагноз:** связка `DSS + MoCo queue + hard_mining_k=512` создаёт **too-hard negatives problem** на холодном энкодере. Каждый компонент по отдельности рассчитан на warm encoder; их композиция на ep1 (когда R@1 ещё ~0.07) делает задачу нерешаемой и приводит к коллапсу embedding space.

**Решение:** curriculum — сначала mutex+CE без queue/mining до R@1≈0.2, потом постепенно включать остальные механизмы.

**Блокер:** диск переполнен (ENOSPC), без освобождения места дальнейшее обучение и сохранение чекпойнтов невозможно.
