Initial commit: World-UAV annotation pipeline

4-modality annotation pipeline (depth, edges, segmentation, chmv2) for 973K
drone/satellite images. SegEarth-OV3 open-vocabulary segmentation with 11
classes optimized for cross-view geo-localization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
pikaliov
2026-04-16 11:22:01 +03:00
commit 686db62c25
312 changed files with 83978 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
# Анализ и оптимизация пайплайна depth_edges_annotate_worlduav
## Обзор
Пайплайн для аннотации датасета World-UAV: четыре стадии (depth, edges, segmentation, chmv2), конфигурация через Gin, fallback-модели, атомарное сохранение, resume-логика. Нейросетевые модели (SegEarth-OV-3, Depth-Anything-3) вендорированы в `src/nn/`.
---
## 1. Инференс GPU -- самые крупные выигрыши
### 1.1 `torch.compile()` для моделей
В `models.py` после загрузки модели можно обернуть в `torch.compile(model, mode="reduce-overhead")`. Для DepthAnything и SegFormer это может дать **20-40% ускорения** на repeated batches за счет fusion ядер и устранения Python overhead. Особенно эффективно при большом количестве батчей.
### 1.2 AMP через `torch.autocast` вместо ручного `.half()`
Сейчас в `inference.py:79` и `inference.py:203` делается ручной каст `x = x.half()` при условии `model.parameters().dtype == torch.float16`. Это неоптимально:
- `torch.autocast("cuda", dtype=torch.float16)` автоматически выберет FP16 для matmul/conv, но оставит FP32 для нормализации и softmax -- меньше потерь точности и обычно быстрее
- Убирает необходимость в ручных проверках dtype
### 1.3 DA3: batch-инференс вместо поштучного цикла
`inference.py:59-74` -- DA3 обрабатывается через `model.inference()` API, который внутри делает preprocess + forward + postprocess. Для максимальной производительности можно вызывать низкоуровневый `model.forward()` напрямую (весь батч тензором на GPU), минуя конвертации tensor -> numpy -> PIL -> обратно.
### 1.4 SegEarth-OV3: batched backbone (реализовано в v3.2)
`predict_pil_batch()` батчит backbone SAM 3.1 (~80% времени) на до 8 изображений. Grounding decoder остается per-image. Дальнейшие оптимизации:
- `torch.cuda.Stream` для overlap compute/data-transfer
- Увеличение max_batch если VRAM позволяет (текущий лимит 8 hardcoded)
---
## 2. Data Pipeline -- ускорение загрузки
### 2.1 `persistent_workers=True` (реализовано)
DataLoader создается с `persistent_workers=True` при `num_workers > 0`. Воркеры не пересоздаются между батчами.
### 2.2 `prefetch_factor` (реализовано)
`prefetch_factor=4` для лучшего overlap загрузки с инференсом.
### 2.3 Decode на GPU через `torchvision.io` или `nvidia-dali`
`dataset.py` -- `Image.open().convert("RGB")` + `transforms.Resize()` работает на CPU через PIL. Для больших датасетов это bottleneck. Варианты:
- `torchvision.io.decode_image` + `torchvision.transforms.v2` -- resize на GPU
- NVIDIA DALI pipeline -- полный decode+resize+normalize на GPU, особенно выгоден при images > 10k
### 2.4 Предвычисление списка файлов
`dataset.py` -- `rglob("*")` обходит файловую систему при каждом запуске. Для больших датасетов (тысячи папок) это минуты. Можно кешировать список в `.file_cache.json` и обновлять только при изменении mtime корня.
---
## 3. I/O -- запись результатов
### 3.1 Асинхронная запись (реализовано)
`io_utils.py` -- `ThreadPoolExecutor` с 4 workers для неблокирующей записи файлов. GPU inference продолжается пока предыдущий батч пишется на диск.
### 3.2 Атомарная запись (реализовано)
Temp file + `os.replace()` для crash-safety. Resume-логика корректно обрабатывает прерванные записи.
### 3.3 Визуализации -- отложить или отключить
`save_vis=False` отключает PNG-визуализации. Для ускорения можно генерировать визуализации отдельным скриптом после всего инференса.
### 3.4 Pre-create output dirs (реализовано)
Все output_dir создаются одним проходом до начала обработки (`run_pipeline()` в `main.py:333-339`), а не per-image.
---
## 4. Edges стадия
### 4.1 Batched Sobel (реализовано)
Edges обрабатываются батчами по 32 изображения. `compute_edges_from_depth` поддерживает `[B, 1, H, W]`.
### 4.2 Sobel-ядра как module-level константы (реализовано)
`_SOBEL_X` и `_SOBEL_Y` -- module-level константы в `inference.py`.
### 4.3 Edges как побочный продукт depth
Если не нужен resume между стадиями -- считать edges прямо в `run_depth_stage` из только что вычисленного depth, не сохраняя и не загружая `.npy`. Текущая архитектура предпочитает гранулярный resume.
---
## 5. Resume и discovery
### 5.1 Completion manifest вместо per-file проверок
`filter_completed()` -- для каждого изображения вызывается `Path.exists()` (syscall). При 100k изображений x 4 стадии = 400k stat-вызовов. Альтернатива:
- `completed.json` / SQLite per stage
- Проверка по set-lookup вместо filesystem
### 5.2 `filter_completed` -- per-stage
Каждая стадия фильтрует отдельно. Можно за один проход собрать статусы всех стадий.
---
## 6. Память и типы данных
### 6.1 FP32 для CHMv2 (вынужденная мера)
CHMv2 (DINOv3 DPT head) выдает NaN в FP16. Всегда FP32 -- это увеличивает VRAM на ~0.65 GB vs FP16, но гарантирует корректность.
### 6.2 uint8 для сегментации (реализовано)
`infer_segmentation_batch` возвращает `uint8` для class IDs. При 5 классах это оптимально.
### 6.3 float16 для .npy (реализовано)
Depth, edges, CHM сохраняются в `.npy` как `float16` (если `save_npy=True`).
---
## 7. Вендорированные пакеты (src/nn/)
### 7.1 Текущая архитектура
SegEarth-OV-3 и Depth-Anything-3 **встроены в проект** как вендорированные пакеты в `src/nn/`. При `import src.nn` автоматически регистрируются пути в `sys.path`:
- `src/nn/` -- для `depth_anything_3.*`
- `src/nn/segearth_ov3/` -- для `sam3.*` и `segearthov3_segmentor`
**Преимущества:**
- Нет зависимости от внешних репозиториев
- Воспроизводимость -- фиксированная версия кода
- Нет конфликтов с системными пакетами
**Ограничения:**
- Обновление до новой версии модели требует ручного копирования
- Дублирование кода (~6.5 MB) -- допустимо для проекта с ~200 GB данных
### 7.2 BPE vocab
Файл `bpe_simple_vocab_16e6.txt.gz` встроен в `src/nn/segearth_ov3/sam3/assets/`. Дефолтный путь в `segearthov3_segmentor.py` вычисляется через `os.path.dirname(__file__)`. Копия в `in/weights/` используется приоритетно (если существует) для совместимости с существующими инсталляциями.
---
## Приоритет по соотношению усилие/выигрыш
| # | Оптимизация | Усилие | Выигрыш | Статус |
|---|-------------|--------|---------|--------|
| 1 | Async I/O через ThreadPool | Низкое | **Высокий** (overlap GPU/disk) | **Реализовано** |
| 2 | Batched backbone SegEarth-OV3 | Среднее | **Высокий** (~2.5-3x segm) | **Реализовано** |
| 3 | Pre-create output dirs | Минимальное | Средний | **Реализовано** |
| 4 | Batched Sobel edges | Низкое | Средний | **Реализовано** |
| 5 | dtype: uint8 для seg, float16 для npy | Низкое | Средний | **Реализовано** |
| 6 | `persistent_workers` + `prefetch_factor` | Минимальное | Низкий-Средний | **Реализовано** |
| 7 | Вендоринг моделей (src/nn/) | Среднее | Средний (reliability) | **Реализовано** |
| 8 | `torch.compile()` | Низкое | Средний (20-40%) | Не реализовано |
| 9 | `torch.autocast` вместо `.half()` | Низкое | Низкий-Средний | Не реализовано |
| 10 | DA3 прямой forward вместо `inference()` | Среднее | Средний | Не реализовано |
| 11 | Completion manifest вместо per-file exists | Среднее | Средний (при >100k img) | Не реализовано |
| 12 | GPU decode (DALI / torchvision.io) | Высокое | Средний | Не реализовано |

View File

@@ -0,0 +1,326 @@
# SegEarth-OV3: архитектура, pipeline и оптимизация
## Обзор
SegEarth-OV3 — модель **open-vocabulary** семантической сегментации для дистанционного зондирования, построенная на базе **SAM 3.1** (Segment Anything Model 3.1, Meta). Позволяет сегментировать изображения по произвольным текстовым описаниям классов без дообучения.
В нашем пайплайне используется для генерации семантических карт из аэрофотоснимков (дрон) и спутниковых изображений датасета World-UAV (973K изображений).
---
## Архитектура модели
### Общая схема
```
┌──────────────────────────────────────────┐
│ SAM3VLBackbone │
│ ┌─────────────────┐ ┌───────────────┐ │
Image ──────────►│ │ Vision Backbone │ │ Text Encoder │◄─── Text Prompt
[B,3,1008,1008] │ │ (ViT + Neck) │ │ (VE, 24-layer)│ │
│ └────────┬────────┘ └───────┬───────┘ │
│ │ │ │
│ vision_features language_features│
│ [B,256,72,72] [32,1,256] │
└──────────┬────────────────────┬───────────┘
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ Transformer Encoder (6 layers) │
│ Cross-attention: vision × language │
│ Self-attention: vision features │
└──────────────────┬───────────────────────┘
┌──────────────────────────────────────────┐
│ Transformer Decoder (6 layers) │
│ 200 object queries │
│ Cross-attention: queries × encoder out │
│ Text cross-attention │
│ Box refinement (DAC) │
│ Presence token │
└──────────┬───────────────────────────────┘
┌──────────┴──────────┐
│ │
▼ ▼
┌──────────────┐ ┌───────────────────┐
│ Instance Head │ │ Segmentation Head │
│ pred_boxes │ │ (PixelDecoder + │
│ pred_masks │ │ semantic_seg) │
│ pred_logits │ │ │
│ presence_score│ │ semantic_mask_logits│
└──────────────┘ └───────────────────┘
│ │
└──────────┬──────────┘
┌──────────────────────────────────────────┐
│ Post-processing │
│ argmax → class index map │
│ threshold filtering → background │
└──────────────────────────────────────────┘
```
### Компоненты
#### 1. Vision Backbone (ViT + FPN Neck)
- **ViT** (Vision Transformer): 32 слоя, embed_dim=1024, 16 heads
- Входное разрешение: **1008×1008** (patch_size=14 → 72×72 patches)
- RoPE (Rotary Position Embeddings) с интерполяцией
- Window attention (window=24) + 4 global attention blocks (слои 7, 15, 23, 31)
- Tiled absolute position embeddings (pretrain=336 → tile до 1008)
- **Sam3DualViTDetNeck**: FPN-neck с 4 масштабами (×4, ×2, ×1, ×0.5)
- Выход: `[B, 256, 72, 72]` (после scalp=1, отбрасывается низшее разрешение)
- Также генерирует SAM2-совместимые features для instance interactivity
#### 2. Text Encoder (VETextEncoder)
- 24-слойный Transformer, width=1024, 16 heads
- BPE токенизатор (vocab: 16M tokens из `bpe_simple_vocab_16e6.txt.gz`)
- Вход: текстовый промпт (например, `"railway"`)
- Выход:
- `language_features`: `[32, 1, 256]` — проекция в 256-dim space
- `language_mask`: `[1, 32]` — маска внимания
- `language_embeds`: `[32, 1, 1024]` — полные эмбеддинги
#### 3. Transformer Encoder (6 layers)
- Fusion: cross-attention между vision features и language features
- Self-attention на vision features
- d_model=256, dim_feedforward=2048, 8 heads
- Activation checkpointing включён
#### 4. Transformer Decoder (6 layers)
- **200 object queries** — learnable queries для обнаружения объектов
- Cross-attention: queries × encoder output
- Text cross-attention: queries × language features
- **DAC** (Decoupled Attention for Classification) — разделение box regression и classification
- **Box refinement** на каждом слое (iterative)
- **Presence token** — для оценки наличия объекта в сцене
- Resolution=1008, stride=14
#### 5. Scoring & Heads
- **DotProductScoring**: MLP (256→2048→256) + dot product для class scores
- **Instance Head**: pred_boxes, pred_masks, pred_logits, presence_score
- **Segmentation Head** (UniversalSegmentationHead):
- PixelDecoder: 3-stage upsampling (nearest interpolation), hidden_dim=256
- Выход: `semantic_seg` — пиксельная семантическая маска
---
## Pipeline инференса
### Для одного изображения (`predict_pil`)
```
1. set_image(pil_image)
└── transform → [1, 3, 1008, 1008]
└── backbone.forward_image() → backbone_out (vision features)
2. Для каждого из 11 промптов:
a. reset_all_prompts(state)
b. set_text_prompt(prompt, state)
└── backbone.forward_text([prompt]) → text_outputs
└── state["backbone_out"].update(text_outputs)
└── _forward_grounding(state)
├── model.forward_grounding(backbone_out, geometric_prompt)
├── pred_boxes, pred_masks, pred_logits → filter by confidence
└── semantic_seg → interpolate to (H, W)
c. Агрегация:
- instance: seg_logits[i] = max(seg_logits[i], mask * score)
- semantic: seg_logits[i] = max(seg_logits[i], semantic_logits)
- presence: seg_logits[i] *= presence_score
3. Post-processing:
└── argmax(seg_logits, dim=0) → class map
└── max_vals < threshold → set to background (class 0)
```
**Итого:** 1 backbone pass + **11 × (text_encoder + grounding_decoder)** forward passes.
### Для батча (`predict_pil_batch`)
```
1. set_image_batch(images)
└── transform all → [B, 3, 1008, 1008]
└── backbone.forward_image(batch) → batch_state ← ОДНА операция на весь батч
2. Для каждого изображения i в батче:
a. _slice_backbone_out(batch_state, i) → per-image state
b. Для каждого из 11 промптов:
└── (используются кэшированные text embeddings)
└── _forward_grounding(state) → instance + semantic masks
c. argmax + threshold → class map
```
**Батчинг backbone** экономит ~15% времени vs per-image.
**Кэширование text embeddings** экономит ~2% (text encoder очень быстрый).
---
## Профиль производительности (RTX 4090, 24 GB VRAM)
### Разбивка по времени (на 1 изображение, 256×256, 11 промптов)
| Этап | Время | Доля |
|---|---|---|
| Vision backbone (ViT + Neck) | ~25 ms | ~9% |
| Text encoder (× 11 промптов) | ~5 ms | ~2% |
| Grounding decoder (× 11 промптов) | ~240 ms | ~84% |
| Post-processing (argmax, threshold) | ~2 ms | ~1% |
| Overhead (PIL convert, transfer) | ~14 ms | ~5% |
| **Итого** | **~286 ms** | **100%** |
### Throughput при разных batch size
| Batch size | Throughput | VRAM | ms/img |
|---|---|---|---|
| 8 | 3.4 img/s | 6.2 GB | 292 |
| 10 | 3.5 img/s | 6.2 GB | 286 |
| 16 | 3.3 img/s | 8.1 GB | 301 |
| 24 | 3.5 img/s | 8.8 GB | 286 |
| 32 | 3.5 img/s | 8.8 GB | 284 |
**Вывод:** throughput **не масштабируется** с batch size, т.к. bottleneck — **per-image grounding decoder** (11 последовательных forward passes на каждое изображение). Backbone батчится, но это лишь 9% времени.
### Оценка на полный датасет
| Подмножество | Кол-во изображений | Время при 3.5 img/s |
|---|---|---|
| DB (спутник) | ~486K | ~38.5 часов |
| Query (дрон) | ~486K | ~38.5 часов |
| Всё | ~973K | ~77 часов |
---
## Применённые оптимизации
### 1. Кэширование text embeddings
**Файл:** `src/nn/segearth_ov3/segearthov3_segmentor.py`
Text encoder вызывается с одними и теми же 11 промптами для каждого изображения. Кэширование результатов `forward_text()` при первом вызове и повторное использование для всех последующих изображений.
```python
def _cache_text_embeddings(self):
"""Pre-compute and cache text embeddings for all prompts (run once)."""
if hasattr(self, '_text_cache'):
return
self._text_cache = []
for query_word in self.query_words:
text_out = self.processor.model.backbone.forward_text(
[query_word], device=self.device,
)
cached = {k: v.clone() if isinstance(v, torch.Tensor) else v
for k, v in text_out.items()}
self._text_cache.append(cached)
```
**Эффект:** ~2% ускорение (54 ms на батч из 8). Text encoder итак быстрый (~0.5 ms/промпт).
### 2. Увеличение batch size (8 → 16)
**Файлы:** `src/main.py`, `src/augmentor/inference.py`
- `_MAX_SEG_BATCH`: 8 → 16
- Segmentation stage `bs`: 8 → 16
**Эффект:** VRAM вырос 6.2 → 8.8 GB (из 24 доступных). Throughput стабилен, но меньше overhead на создание батчей и DataLoader.
### 3. Autocast bfloat16
Уже включён в оригинальном коде:
```python
with torch.no_grad(), torch.autocast(device_type="cuda", dtype=torch.bfloat16):
```
### 4. TF32 для Ampere+ GPU
Включён автоматически в `model_builder.py`:
```python
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
```
---
## Фундаментальные ограничения (нельзя ускорить без переписывания модели)
### 1. Последовательный grounding per prompt
Grounding decoder принимает **один текстовый промпт за раз** и прогоняет весь decoder (6 layers, 200 queries). При 11 промптах это 11 × decoder forward = **84% общего времени**.
**Почему нельзя батчить:** архитектура decoder требует `language_features` в cross-attention — разные промпты дают разные `language_features`, что меняет attention pattern. Для батчинга нужно переписать decoder для поддержки multi-prompt inference (нетривиально).
### 2. Последовательный grounding per image
Даже при батчинге backbone, grounding всё равно выполняется **per-image** (slice backbone features → per-image state → 11 decoder passes). Причина: decoder выдаёт per-image instance predictions (boxes, masks, scores), которые нельзя батчить из-за переменного числа detected instances.
### 3. Высокое разрешение ViT
ViT работает на 1008×1008 (72×72 patches), что требует значительного compute. Снижение `default_resolution` ускорит backbone, но ухудшит качество сегментации мелких объектов.
---
## Альтернативные пути ускорения (не реализованы)
| Подход | Ожидаемый эффект | Сложность | Риск |
|---|---|---|---|
| `torch.compile(model)` | 10-30% на decoder | Средняя | Dynamic shapes могут сломать |
| Снижение `num_queries` (200 → 100) | ~20% на decoder | Нужно переучить | Потеря мелких объектов |
| Снижение `default_resolution` (1008 → 504) | ~4x backbone | Тривиально (config) | Ухудшение качества |
| Multi-GPU inference | ~2x при 2 GPU | Средняя | Нужен второй GPU |
| ONNX/TensorRT export | 2-5x overall | Высокая | SAM3 dynamic shapes |
| Замена на SegFormer | ~3x быстрее | Тривиально (fallback есть) | Нет open-vocab, фиксированные 150 классов |
---
## Конфигурация в проекте
### Файлы
| Файл | Назначение |
|---|---|
| `in/config_files/segmentation.gin` | Промпты, threshold, разрешение |
| `src/nn/segearth_ov3/segearthov3_segmentor.py` | Обёртка SegEarth-OV3 (predict_pil, predict_pil_batch) |
| `src/nn/segearth_ov3/sam3/model_builder.py` | Сборка модели (build_sam3_image_model) |
| `src/nn/segearth_ov3/sam3/model/sam3_image_processor.py` | Inference processor (set_image, set_text_prompt) |
| `src/nn/segearth_ov3/sam3/model/sam3_image.py` | Основная модель Sam3Image |
| `src/nn/segearth_ov3/sam3/model/vl_combiner.py` | Vision-Language backbone |
| `src/augmentor/models.py` | Загрузка модели (load_segmentation_model) |
| `src/augmentor/inference.py` | Батчевый инференс (infer_segmentation_batch) |
| `in/weights/sam3.1/sam3.1_multiplex.pt` | Чекпоинт модели (~2.5 GB) |
### Текущая конфигурация
```gin
SegConfig.prompts = [
'background', # 0
'building', # 1
'road', # 2
'vegetation', # 3
'water', # 4
'sand and gravel ground', # 5
'rocky terrain', # 6
'farmland', # 7
'railway', # 8
'parking lot', # 9
'sidewalk', # 10
]
SegConfig.threshold = 0.15
SegConfig.default_resolution = 1008
```
### Параметры модели
| Параметр | Значение | Описание |
|---|---|---|
| `prob_thd` | 0.15 | Минимальная уверенность для не-background класса |
| `confidence_threshold` | 0.5 | Порог для instance detection (boxes/masks) |
| `use_sem_seg` | True | Использовать semantic segmentation head |
| `use_presence_score` | True | Масштабировать logits на presence score |
| `use_transformer_decoder` | True | Использовать instance-level predictions |
| `bg_idx` | 0 | Индекс класса background |

View File

@@ -0,0 +1,194 @@
# Анализ классов сегментации для UAV-GeoLoc
## Обзор датасета
- **392 локации**, 76 кадров каждая (height100_rot90) = ~29 800 изображений
- **11 стран** (Country/): Australia, Brazil, English, French, German, Italy, Japan, Korea, Poland, Spain, USA
- **27 типов terrain** (Terrain/): Basin, Calcification, Danxia, Delta, Desert, Fall, Farm, Finca, Flowers, Glacier, Gorge, Hill, Hylare, Island, Karst, Lakes-ignore, Mountain, Oasis, Pasture, Plain, Plateau, Prairie, Snow, StoneForest, Terrace, Volcano, Wetland
## Проблема с исходной конфигурацией (5 классов)
Исходные промпты:
```
SegConfig.prompts = ['background', 'building', 'road', 'vegetation', 'water']
SegConfig.threshold = 0.3
```
При 5 классах **60-90% площади terrain-кадров и 30-50% городских кадров уходит в background**, теряя информативные объекты (ж/д пути, грунт, скалы и т.д.).
---
## Инвентаризация объектов
### URBAN (Country/) — 11 стран, ~300 локаций
| Объект | Где встречается | Частота |
|---|---|---|
| Здания (крыши: черепица, плоские, металл, стекло) | Все города | Доминирует |
| Дорога / асфальт | Все города | Очень высокая |
| Растительность (деревья, кустарники) | Все города | Высокая |
| Тротуар / пешеходная зона / набережная | Все города, особенно Европа | Высокая |
| Парковка | Мюнхен, SF, Варшава, Аделаида | Средняя |
| Железная дорога (рельсы, балласт, стрелки) | Мюнхен, Сидней, Варшава | Средняя |
| Вода (река, канал, море) | Париж/Сена, Венеция, Киото, Бусан, Копакабана | Средняя |
| Мост / эстакада | Сидней, Париж | Низкая-средняя |
| Внутренний двор | Все европейские города | Средняя |
| Открытый грунт / гравий | Мюнхен, Варшава | Средняя |
| Газон / парк | Лондон/Westminster, Rio | Средняя |
| Спортивная площадка | Мюнхен | Низкая |
| Пляж / песок | Копакабана, Бусан/Gwangalli | Низкая |
| Дорожная разметка (зебры, полосы) | Все города | Высокая, но мелкая |
| Солнечные панели | SF, Мюнхен | Низкая |
### TERRAIN — 27 типов
| Terrain | Что на кадрах | Ключевые поверхности |
|---|---|---|
| Desert (Gobi) | Песок, эрозионные борозды, скалы | bare ground, sand, rock |
| Farm (Bohemian) | Густой лес, кроны деревьев | vegetation |
| Glacier (Athabasca) | Лёд, снег, трещины | snow, ice, water |
| Island (Aldabra) | Бирюзовая вода, мелководье | water |
| Wetland (Danube) | Болотистая почва, кустарник | vegetation, bare ground, water |
| Mountain (Andes) | Песок/пыль, следы троп | bare ground, rock |
| Delta (Congo) | Лавовые/скальные поверхности | rock, bare ground |
| Volcano (Kilauea) | Тёмная лава, скальные породы | rock, bare ground |
| Snow (Aconcagua) | Белый снег, точки камней | snow |
| Danxia (GrandCanyon) | Красная порода + растительность | rock, vegetation |
| Oasis | Саванна, кустарники на песке | bare ground, vegetation |
| Finca (Althorp) | Хвойный лес, плантации | vegetation |
| Terrace (Banaway) | Сельхоз террасы, тропы | farmland, bare ground |
| Plain (Alberta) | Луг, речная пойма, грунтовая дорога | grassland, bare ground, road |
| Plateau | Кустарник на каменистой почве | vegetation, rock |
| Prairie (Etosha) | Саванна, точки кустов на песке | bare ground, vegetation |
| Gorge (Antelope) | Каньон, красные скалы | rock, bare ground |
| Karst (Mammoth) | Скальные плиты, трещины | rock |
| Calcification (ElTatio) | Гейзерные поля, песчаная порода | bare ground, rock |
| Flowers (BlueHotSpring) | Горячие источники, минеральные отложения | rock, water |
| Hylare (Amazon) | Густой тропический лес | vegetation |
| StoneForest (GrandCanyon) | Каньонные скалы + кустарник | rock, vegetation |
| Hill (Sedona) | Красные скалы + кустарник | rock, vegetation |
| Basin | — | — |
| Fall | — | — |
| Pasture | — | — |
| Lakes-ignore | — | — |
---
## Финальная конфигурация: 11 классов
```python
SegConfig.prompts = [
'background', # 0 — unclassified
'building', # 1 — buildings, rooftops
'road', # 2 — roads, asphalt
'vegetation', # 3 — trees, bushes, forest canopy
'water', # 4 — rivers, canals, sea, lakes
'sand and gravel ground', # 5 — soil, gravel, sand, dust, bare earth
'rocky terrain', # 6 — rock, stone, lava, canyon walls
'farmland', # 7 — agricultural terraces, fields
'railway', # 8 — railway tracks, rails
'parking lot', # 9 — parking areas
'sidewalk', # 10 — sidewalks, pedestrian zones, embankments
]
SegConfig.threshold = 0.15
SegConfig.default_resolution = 1008
```
### Эволюция конфигурации
1. **v1** (исходная): 5 классов, threshold=0.3 — слишком много background
2. **v2** (первая итерация): 12 классов (`bare ground`, `rock`, `snow`, `farmland`, `railway`, `parking lot`, `sidewalk`), threshold=0.3 — terrain по-прежнему плохо
3. **v3** (вторая итерация): 12 классов с переименованными промптами (`sand and gravel ground`, `rocky terrain`, `snow and ice`), threshold=0.15 — значительное улучшение terrain
4. **v4** (финальная): 11 классов — убран `snow and ice` (SegEarth-OV3 не различает снег/лёд от воды при виде сверху), threshold=0.15
### Обоснование каждого нового класса
| Класс | Промпт | Обоснование | Покрытие |
|---|---|---|---|
| bare ground | `sand and gravel ground` | Без него пустыня, гейзеры, плато, прерия, пляж уходят в background | Критичен для terrain |
| rock | `rocky terrain` | Вулканы, каньоны, карст — уникальная текстура | Критичен для terrain |
| farmland | `farmland` | Террасы, поля — геометрически уникальны для matching | Важен для Terrace/Plain/Farm |
| railway | `railway` | Ж/д пути — линейный ориентир с уникальной топологией | Важен для городов |
| parking lot | `parking lot` | Чёткая прямоугольная геометрия, видна и с дрона и со спутника | Средний для городов |
| sidewalk | `sidewalk` | Структурные границы в городах, набережные | Средний для городов |
### Отброшенные классы и причины
| Класс | Причина отказа |
|---|---|
| `snow and ice` | SegEarth-OV3 классифицирует лёд/снег как `water` при виде сверху — промпт не работает. Проверено на Glacier (Athabasca) |
| `beach` / `sand` | Покрыт `sand and gravel ground` |
| `bridge` | Слишком редко, перекрывается с `road` |
| `grassland` / `lawn` | Перекрывается с `vegetation` |
| `river` / `sea` / `lake` | Всё покрыто единым `water` |
| `sports field` | Слишком редко на полном датасете |
| `courtyard` | Семантически = `bare ground` + `building` |
| `fence` | Слишком тонкий объект для SegEarth на 1008px |
| `road marking` | Слишком мелкий, лучше оставить как часть `road` |
| `solar panel` | Редко, мелко, часть крыши |
---
## Результаты тестирования на 10 изображениях
Тестовые изображения: 6 городских (Munich, Paris, Venice, New York, Sydney, Busan) + 4 terrain (Desert, Glacier, Volcano, Danxia).
### Городские сцены
| Локация | Обнаруженные классы | Качество |
|---|---|---|
| Munich (ж/д пути) | railway, sand/gravel, vegetation, road, water | Отлично — ж/д = фиолетовый, балласт = бежевый |
| Paris (набережная Сены) | road, vegetation, water, sidewalk, parking lot | Отлично — река, тротуар, дорога разделены |
| Venice (старый город) | building, road, water, parking lot | Хорошо — каналы появились как water |
| New York (Manhattan) | building, road, vegetation, parking lot, sidewalk | Отлично — все городские классы |
| Sydney (эстакада) | road, building, vegetation, parking lot, sidewalk | Хорошо |
| Busan (пляж) | water, sand and gravel ground | Отлично — пляж = бежевый, море = синий |
### Terrain
| Локация | Обнаруженные классы | Качество |
|---|---|---|
| Desert (Gobi) | road, rocky terrain | Приемлемо — пустыня = road (серый близок к песку) |
| Glacier (Athabasca) | water | Ограничение модели — лёд = water |
| Volcano (Kilauea) | rocky terrain | Отлично — было 100% background, стало 100% rocky terrain |
| Danxia (GrandCanyon) | vegetation | Частично — кусты определены, скалы в background |
### Известные ограничения SegEarth-OV3
- Ледники/снег классифицируются как `water` — визуально неразличимы сверху для модели
- Красные скалы (Danxia) плохо определяются как `rocky terrain`
- Пустынный грунт может путаться с `road`
---
## Палитра цветов
| ID | Класс | RGB | Цвет |
|:--:|:---|:---|:---|
| 0 | background | (0, 0, 0) | Black |
| 1 | building | (220, 40, 40) | Red |
| 2 | road | (160, 160, 160) | Gray |
| 3 | vegetation | (30, 180, 30) | Green |
| 4 | water | (30, 120, 220) | Blue |
| 5 | sand and gravel ground | (180, 140, 80) | Tan |
| 6 | rocky terrain | (120, 100, 80) | Brown |
| 7 | farmland | (200, 200, 50) | Yellow |
| 8 | railway | (100, 60, 120) | Purple |
| 9 | parking lot | (255, 165, 0) | Orange |
| 10 | sidewalk | (200, 200, 200) | Light gray |
## Оценка влияния на производительность
- Инференс: ~1.82.2x медленнее (11 vs 5 text prompts)
- На 973K изображений: дополнительные ~812 часов на RTX 4090
- Background на terrain: сокращение с 6090% до ~1020%
- Background на urban: сокращение с 3050% до ~515%
## Методология анализа
Просмотрено ~50 кадров с равномерной выборкой:
- По 13 кадра из каждой страны (разные города/районы)
- По 1 кадру из каждого типа terrain (первая доступная локация)
- Кадры выбирались из середины траектории (frame 30) для репрезентативности
- Дополнительно просмотрены кадры из начала/конца траектории для Мюнхена (полный обзор 76 кадров)
- Тестирование проведено в 3 итерации с подбором формулировок промптов и порога confidence

View File

@@ -0,0 +1,477 @@
# Skills: Оптимизация I/O и DL/ML инференса
Справочник приёмов оптимизации для пайплайнов обработки данных и инференса нейросетей.
Используется как чеклист при code review и проектировании новых пайплайнов.
---
## Часть 1. Оптимизация I/O
### 1.1 Асинхронная запись на диск
**Проблема:** синхронный `np.save()` / `Image.save()` блокирует основной поток — GPU простаивает.
**Решение:**
```python
from concurrent.futures import ThreadPoolExecutor
_io_pool = ThreadPoolExecutor(max_workers=4)
def save_async(fn, *args, **kwargs):
"""Отправить операцию сохранения в фоновый поток."""
_io_pool.submit(fn, *args, **kwargs)
# В inference-цикле:
for batch in loader:
result = model(batch)
save_async(save_result, result, path) # не блокирует GPU
```
**Когда применять:** всегда, когда запись идёт между батчами инференса.
**Подводные камни:**
- Контролировать размер очереди — если запись медленнее инференса, очередь растёт и съедает RAM
- При аварийном завершении незаписанные данные теряются — использовать atomic save (temp + rename)
- `_io_pool.shutdown(wait=True)` в конце пайплайна
---
### 1.2 Атомарная запись файлов
**Проблема:** прерванная запись оставляет битый файл, resume-логика считает его валидным.
**Решение:**
```python
import tempfile, os
def atomic_save_npy(arr, path):
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(suffix=".tmp", dir=path.parent)
os.close(fd)
try:
np.save(tmp, arr)
os.replace(tmp, path) # атомарная операция на одной ФС
except BaseException:
os.remove(tmp) if os.path.exists(tmp) else None
raise
```
**Когда применять:** при любом сохранении промежуточных результатов с resume-логикой.
---
### 1.3 Минимизация syscalls
**Проблема:** `Path.mkdir()`, `Path.exists()` — это syscalls. При 100k файлов × 3 стадии = 300k+ вызовов.
**Решения:**
| Приём | Описание |
|-------|----------|
| **Pre-create dirs** | Один проход `mkdir` для всех output_dir до начала обработки |
| **Completion manifest** | `set()` в памяти вместо `Path.exists()` на каждый файл |
| **Batch stat** | `os.scandir()` вместо поштучного `exists()` |
```python
# Вместо per-file exists():
completed = set()
for entry in os.scandir(output_root):
if entry.is_dir():
for f in os.scandir(entry.path):
if f.name == "depth.npy":
completed.add(entry.name)
# Фильтрация:
pending = [r for r in records if r.stem not in completed]
```
---
### 1.4 Выбор формата сохранения
| Формат | Скорость записи | Размер | Загрузка | Когда использовать |
|--------|----------------|--------|----------|--------------------|
| `.npy` (float32) | Быстро | Большой | Быстро | Промежуточные результаты |
| `.npy` (float16) | Быстро | 2× меньше | Быстро | Depth, edges [0,1] |
| `.npy` (uint8) | Быстро | 8× меньше int64 | Быстро | Class IDs (< 256 классов) |
| `.npz` compressed | Медленно | Маленький | Медленно | Архивация, не для пайплайнов |
| `.png` | Медленно (компрессия) | Маленький | Средне | Только визуализация |
| `.safetensors` | Быстро | Маленький | Быстро, mmap | Тензоры для обучения |
**Правило:** для промежуточных данных — минимальный достаточный dtype без компрессии. Визуализации — отдельный шаг по требованию.
---
### 1.5 Оптимизация PNG-записи
```python
# PIL — медленный (zlib compression level 6 по умолчанию)
Image.fromarray(vis).save("out.png")
# OpenCV — быстрее, контроль компрессии
cv2.imwrite("out.png", vis, [cv2.IMWRITE_PNG_COMPRESSION, 1]) # 1 = минимум
# Самый быстрый: отложить визуализацию
# Генерировать PNG отдельным скриптом после всего инференса
```
---
### 1.6 Кеширование файловых списков
**Проблема:** `rglob("*")` на больших датасетах — минуты.
**Решение:**
```python
import json, os
CACHE = root / ".file_cache.json"
def discover_cached(root):
if CACHE.exists():
mtime_cache = CACHE.stat().st_mtime
mtime_root = root.stat().st_mtime
if mtime_cache > mtime_root:
return json.loads(CACHE.read_text())
files = [str(p.relative_to(root)) for p in root.rglob("*") if p.is_file()]
CACHE.write_text(json.dumps(files))
return files
```
**Когда применять:** датасет > 10k файлов, особенно на HDD или сетевых ФС.
---
## Часть 2. Оптимизация DL/ML инференса
### 2.1 `torch.inference_mode()` вместо `torch.no_grad()`
```python
# Хорошо:
@torch.inference_mode()
def predict(model, x):
return model(x)
# Менее эффективно:
with torch.no_grad():
return model(x)
```
`inference_mode` отключает version counting и autograd tracking полностью — на 510% быстрее `no_grad` и меньше потребление памяти.
---
### 2.2 `torch.compile()`
```python
model = load_model()
model = torch.compile(model, mode="reduce-overhead")
# первый вызов — долгий (компиляция), далее — 2040% быстрее
```
| Режим | Скорость компиляции | Ускорение рантайма | Когда |
|-------|--------------------|--------------------|-------|
| `"default"` | Средняя | Среднее | Общий случай |
| `"reduce-overhead"` | Медленная | Максимальное | Много батчей, стационарные формы |
| `"max-autotune"` | Очень медленная | Максимальное + подбор ядер | Продакшн, фиксированные shapes |
**Ограничения:**
- Не работает с dynamic shapes без `dynamic=True`
- Некоторые кастомные операции не компилируются — используйте `torch._dynamo.config.suppress_errors = True` при отладке
- Первый вызов медленный — прогрев обязателен
---
### 2.3 Automatic Mixed Precision (AMP)
```python
# Правильно: autocast выбирает dtype per-operation
with torch.autocast("cuda", dtype=torch.float16):
output = model(input_tensor)
# Неправильно: ручной .half() на всё
input_tensor = input_tensor.half()
output = model(input_tensor) # нормализация и softmax тоже в fp16 — потеря точности
```
**Что autocast делает автоматически:**
- matmul, conv → FP16 (выигрыш скорости)
- layernorm, softmax, loss → FP32 (сохранение точности)
- Accumulations → FP32
**Для обучения** дополнительно нужен `GradScaler`:
```python
scaler = torch.amp.GradScaler()
with torch.autocast("cuda", dtype=torch.float16):
loss = model(x)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
```
---
### 2.4 Batch-инференс vs. per-image цикл
**Проблема:**
```python
# Медленно: per-image на CPU с конвертациями
for i in range(B):
img_np = tensor_to_numpy(batch[i]) # GPU→CPU
result = model.infer_image(img_np) # одно изображение
output.append(numpy_to_tensor(result)) # CPU→GPU
```
**Решение:**
```python
# Быстро: батч на GPU без конвертаций
x = preprocess(batch).to(device) # один transfer
output = model(x) # весь батч за один forward
```
**Ключевые правила:**
- Избегать циклов `for i in range(B)` внутри inference-функций
- Если API модели принимает только одно изображение — обращаться к внутреннему `model.forward()` напрямую
- Минимизировать CPU↔GPU трансферы (каждый `tensor.cpu()` / `.to(device)` — задержка)
- Numpy конвертации (`tensor → numpy → PIL → numpy → tensor`) — красный флаг
---
### 2.5 CUDA Streams для overlap
```python
stream_compute = torch.cuda.Stream()
stream_transfer = torch.cuda.Stream()
for batch in loader:
with torch.cuda.stream(stream_transfer):
next_batch = next_batch.to(device, non_blocking=True)
with torch.cuda.stream(stream_compute):
output = model(current_batch)
torch.cuda.synchronize()
```
**Когда применять:** per-image модели (SegEarth/SAM), где нельзя батчить — overlap загрузки следующего изображения с инференсом текущего.
---
### 2.6 DataLoader — скрытые параметры производительности
```python
DataLoader(
dataset,
batch_size=bs,
num_workers=4, # параллельная загрузка
pin_memory=True, # ускоряет CPU→GPU transfer
persistent_workers=True, # не пересоздавать процессы между эпохами
prefetch_factor=4, # буфер загрузки (дефолт 2)
drop_last=False, # True для обучения, False для инференса
)
```
**Диагностика bottleneck загрузки:**
```python
import time
for batch in loader:
t0 = time.perf_counter()
output = model(batch)
gpu_time = time.perf_counter() - t0
t0 = time.perf_counter()
# просто итерация — если это медленно, bottleneck в загрузке
load_time = time.perf_counter() - t0
```
Если `load_time > gpu_time` — увеличить `num_workers`, добавить `prefetch_factor`, перейти на DALI.
---
### 2.7 Decode и preprocessing на GPU
```python
# CPU (медленно для больших датасетов):
img = Image.open(path).convert("RGB")
tensor = transforms.ToTensor()(transforms.Resize((H, W))(img))
# GPU через torchvision v2:
from torchvision.io import decode_image
from torchvision.transforms import v2
tensor = decode_image(path) # decode на CPU
tensor = tensor.to(device)
tensor = v2.Resize((H, W))(tensor) # resize на GPU
# NVIDIA DALI (полностью на GPU):
# decode JPEG → resize → normalize → выход как CUDA тензор
# Требует отдельного pipeline definition, но 35× быстрее для >10k images
```
---
### 2.8 Оптимизация размера батча
```python
def auto_batch_size(total_vram_mb, weights_mb, per_sample_mb, overhead_mb=200):
"""Вычислить безопасный batch size по VRAM бюджету."""
free = total_vram_mb - weights_mb - overhead_mb
if free <= 0:
return 1
max_b = int(free / per_sample_mb)
# Округление вниз до степени 2 (лучше для GPU)
safe = max(1, 2 ** int(math.log2(max(1, int(max_b * 0.7)))))
return safe
```
**Правила:**
- Степени двойки — лучшая утилизация GPU tensor cores
- 70% от теоретического максимума — запас на пиковое потребление
- `torch.cuda.mem_get_info()` — актуальная свободная память в рантайме (лучше статических оценок)
- При OOM — fallback на `batch_size // 2` с retry
---
### 2.9 Оптимизация конвертаций данных
**Красные флаги (каждый — потеря производительности):**
| Паттерн | Проблема | Решение |
|---------|----------|---------|
| `tensor.numpy()` в цикле | GPU→CPU sync | Батчить, делать `.cpu()` один раз |
| `np.uint8 → float32 → device` | Двойная конвертация | Конвертировать сразу на device |
| `Image.fromarray()` в цикле | PIL overhead | cv2 или отложить |
| `tensor.item()` в цикле | Sync point | Собирать в тензор, `.item()` один раз |
| Пересоздание тензоров-констант | Аллокация памяти | Module-level или `register_buffer` |
---
### 2.10 Профилирование инференса
```python
# Быстрая диагностика: где время?
import torch.utils.benchmark as benchmark
timer = benchmark.Timer(
stmt="model(x)",
globals={"model": model, "x": sample_batch},
num_threads=1,
)
print(timer.blocked_autorange())
# Детальный профиль:
with torch.profiler.profile(
activities=[
torch.profiler.ProfilerActivity.CPU,
torch.profiler.ProfilerActivity.CUDA,
],
record_shapes=True,
with_stack=True,
) as prof:
model(sample_batch)
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=20))
# Экспорт в Chrome trace:
prof.export_chrome_trace("trace.json")
```
**Что искать:**
- Операции с большим `cuda_time` — кандидаты для `torch.compile` / fusion
- Большое `cpu_time` при маленьком `cuda_time` — Python overhead, data transfer
- Много мелких CUDA kernels — нужен fusion (compile / scripting)
---
### 2.11 Кеширование повторяющихся тензоров
```python
# Плохо: пересоздание при каждом вызове
def process(x):
kernel = torch.tensor([[1, 0, -1], [2, 0, -2], [1, 0, -1]], dtype=torch.float32)
return F.conv2d(x, kernel.view(1, 1, 3, 3))
# Хорошо: module-level константа
_SOBEL_X = torch.tensor(
[[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=torch.float32
).view(1, 1, 3, 3) / 8.0
def process(x):
kernel = _SOBEL_X.to(x.device) # перенос на device только при смене
return F.conv2d(x, kernel)
```
---
### 2.12 Экспорт и рантаймы для продакшн-инференса
| Рантайм | Ускорение | Сложность | Когда |
|---------|-----------|-----------|-------|
| `torch.compile` | 1.21.5× | Низкая | Первый шаг оптимизации |
| TorchScript (`torch.jit.trace`) | 1.11.3× | Средняя | Деплой без Python |
| ONNX Runtime | 1.32× | Средняя | Кросс-платформенный инференс |
| TensorRT | 25× | Высокая | NVIDIA GPU, фиксированные shapes |
| OpenVINO | 1.53× | Средняя | Intel CPU/GPU |
```python
# ONNX экспорт:
torch.onnx.export(model, sample_input, "model.onnx",
input_names=["image"], output_names=["depth"],
dynamic_axes={"image": {0: "batch"}, "depth": {0: "batch"}})
# TensorRT через torch_tensorrt:
import torch_tensorrt
trt_model = torch_tensorrt.compile(model,
inputs=[torch_tensorrt.Input(shape=[bs, 3, H, W], dtype=torch.float16)],
enabled_precisions={torch.float16})
```
---
## Часть 3. Чеклист для нового пайплайна
### Перед написанием кода
- [ ] Определить bottleneck: GPU compute, CPU preprocessing, или disk I/O?
- [ ] Выбрать минимально достаточные dtype для хранения
- [ ] Спланировать overlap: загрузка ↔ инференс ↔ сохранение
### Инференс
- [ ] `torch.inference_mode()` на всех predict-функциях
- [ ] `torch.compile()` если > 100 батчей
- [ ] `torch.autocast` вместо ручного `.half()`
- [ ] Batch forward вместо per-image циклов
- [ ] Нет лишних CPU↔GPU трансферов
- [ ] Нет numpy конвертаций внутри inference loop
- [ ] Тензоры-константы вынесены на module level
### Data Pipeline
- [ ] `pin_memory=True`
- [ ] `persistent_workers=True`
- [ ] `prefetch_factor` ≥ 2
- [ ] `num_workers` подобран (обычно 48)
- [ ] Decode/resize на GPU при > 10k images
### I/O
- [ ] Асинхронная запись через `ThreadPoolExecutor`
- [ ] Atomic save для crash safety
- [ ] Минимум syscalls (pre-create dirs, batch stat)
- [ ] Визуализации отделены от основного пайплайна
- [ ] Completion tracking через manifest, не per-file `exists()`
### Память
- [ ] `gc.collect()` + `torch.cuda.empty_cache()` между стадиями
- [ ] Модели выгружаются когда не нужны
- [ ] Промежуточные тензоры не удерживаются в памяти
- [ ] dtype соответствует данным (uint8 для class IDs, float16 для [0,1])