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:
163
docs/analysis_optimization.md
Normal file
163
docs/analysis_optimization.md
Normal 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) | Высокое | Средний | Не реализовано |
|
||||
326
docs/segearth_ov3_architecture.md
Normal file
326
docs/segearth_ov3_architecture.md
Normal 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 |
|
||||
194
docs/segmentation_class_analysis.md
Normal file
194
docs/segmentation_class_analysis.md
Normal 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.8–2.2x медленнее (11 vs 5 text prompts)
|
||||
- На 973K изображений: дополнительные ~8–12 часов на RTX 4090
|
||||
- Background на terrain: сокращение с 60–90% до ~10–20%
|
||||
- Background на urban: сокращение с 30–50% до ~5–15%
|
||||
|
||||
## Методология анализа
|
||||
|
||||
Просмотрено ~50 кадров с равномерной выборкой:
|
||||
- По 1–3 кадра из каждой страны (разные города/районы)
|
||||
- По 1 кадру из каждого типа terrain (первая доступная локация)
|
||||
- Кадры выбирались из середины траектории (frame 30) для репрезентативности
|
||||
- Дополнительно просмотрены кадры из начала/конца траектории для Мюнхена (полный обзор 76 кадров)
|
||||
- Тестирование проведено в 3 итерации с подбором формулировок промптов и порога confidence
|
||||
477
docs/skills_optimization_io_dl_ml.md
Normal file
477
docs/skills_optimization_io_dl_ml.md
Normal 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 полностью — на 5–10% быстрее `no_grad` и меньше потребление памяти.
|
||||
|
||||
---
|
||||
|
||||
### 2.2 `torch.compile()`
|
||||
|
||||
```python
|
||||
model = load_model()
|
||||
model = torch.compile(model, mode="reduce-overhead")
|
||||
# первый вызов — долгий (компиляция), далее — 20–40% быстрее
|
||||
```
|
||||
|
||||
| Режим | Скорость компиляции | Ускорение рантайма | Когда |
|
||||
|-------|--------------------|--------------------|-------|
|
||||
| `"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, но 3–5× быстрее для >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.2–1.5× | Низкая | Первый шаг оптимизации |
|
||||
| TorchScript (`torch.jit.trace`) | 1.1–1.3× | Средняя | Деплой без Python |
|
||||
| ONNX Runtime | 1.3–2× | Средняя | Кросс-платформенный инференс |
|
||||
| TensorRT | 2–5× | Высокая | NVIDIA GPU, фиксированные shapes |
|
||||
| OpenVINO | 1.5–3× | Средняя | 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` подобран (обычно 4–8)
|
||||
- [ ] 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])
|
||||
Reference in New Issue
Block a user