diff --git a/code-style/SKILL.md b/code-style/SKILL.md new file mode 100644 index 0000000..0a7456f --- /dev/null +++ b/code-style/SKILL.md @@ -0,0 +1,220 @@ +--- +name: code-style +description: "Enforce gin-config coding standards for DL/CV research code (Cross-View Geo-Localization). Auto-activates when writing or reviewing Python code. Covers: gin-config pattern, type hints, VRAM management, atomic writes, module structure." +user-invocable: true +allowed-tools: Read Write Edit Glob Grep Bash +argument-hint: "[write|review|refactor] [file-or-module-path]" +--- + +# Стандарт написания кода для DL/CV исследований (CVGL) + +Ты — Machine Learning Engineer, специализирующийся на Computer Vision и Deep Learning (Cross-View Geo-Localization). Пиши, рефактори или ревью код строго по правилам ниже. + +## Режимы + +- `/code-style write ` — написать новый модуль по стандарту +- `/code-style review ` — проверить существующий код на соответствие +- `/code-style refactor ` — привести существующий код к стандарту + +## 1. Окружение и язык + +- **Python 3.10+**, **PyTorch 2.x** +- Первая строка каждого файла: `from __future__ import annotations` +- Весь код, переменные, комментарии — **строго на английском** +- Импорты: stdlib → third-party → local, разделены пустыми строками + +## 2. Типизация и документация + +- **Strict type hints** на всех аргументах функций и return types +- `-> None`, `-> torch.Tensor`, `-> dict[str, Any]` и т.д. +- **Google-style docstrings** на всех публичных классах и функциях: + +```python +def infer_depth(model: nn.Module, images: torch.Tensor) -> torch.Tensor: + """Run monocular depth estimation on a batch. + + Args: + model: Loaded depth model (DA3 or DA V2). + images: Input RGB tensor [B, 3, H, W] float32 [0, 1]. + + Returns: + Depth maps [B, 1, H, W] float32 [0, 1], per-frame normalized. + + Raises: + RuntimeError: If model inference fails. + """ +``` + +## 3. Конфигурация: Gin-Config Strict Pattern + +### 3.1 Классы конфигурации + +- Декоратор `@gin.configurable` **только на классах** (не на функциях) +- **Запрещено** использовать `dataclass` совместно с gin +- Все параметры имеют значения по умолчанию в `__init__` +- Хранение через `self.param = param` + +```python +import gin + +@gin.configurable +class HardwareConfig: + """GPU hardware profile for the augmentation pipeline.""" + + def __init__( + self, + profile_name: str = "rtx4090", + use_fp16: bool = True, + batch_size: int | None = None, + num_workers: int = 4, + reserve_gb: float = 2.0, + ) -> None: + self.profile_name = profile_name + self.use_fp16 = use_fp16 + self.batch_size = batch_size + self.num_workers = num_workers + self.reserve_gb = reserve_gb + # Derived values OK in __init__: + self.total_ram_gb = 24.0 # RTX 4090 + self.available_gb = self.total_ram_gb - self.reserve_gb +``` + +### 3.2 Функции-загрузчики + +```python +def get_hardware_cfg(path2cfg: str) -> HardwareConfig: + """Load hardware config from gin file.""" + gin.parse_config_file(f"{path2cfg}hardware.gin") + return HardwareConfig() +``` + +- Имя: `get__cfg(path2cfg: str) -> ` +- Принимает путь к директории с конфигами (со слешем) +- Вызывает `gin.parse_config_file()` + создаёт экземпляр + +### 3.3 Формат .gin файлов + +```gin +# hardware.gin +HardwareConfig.profile_name = 'rtx4090' +HardwareConfig.use_fp16 = True +HardwareConfig.batch_size = 32 +HardwareConfig.num_workers = 4 +``` + +- **Одна строка — один параметр** (`ClassName.param = value`) +- **Запрещено:** макросы, ссылки, `gin.constant()`, `gin.register()` +- Каждый `.gin` → один конфиг-класс + +### 3.4 Передача конфигов + +```python +def main() -> None: + path2cfg = f"{get_proj_dir()}in/config_files/" + pipeline_conf = get_pipeline_cfg(path2cfg) + hardware_conf = get_hardware_cfg(path2cfg) + run_pipeline(pipeline_conf, hardware_conf) +``` + +- Конфиги загружаются в `main()` через `get_*_cfg()` +- Передаются **явно** как аргументы (не через глобальный gin state) +- **Запрещён argparse** — все параметры из .gin файлов + +## 4. DL/CV практики + +### 4.1 Управление VRAM (24 GB RTX 4090) + +```python +# Sequential model loading pattern: +model = load_model(device) +try: + process_all_images(model, dataset) +finally: + del model + gc.collect() + torch.cuda.empty_cache() +``` + +- Одновременно на GPU — **только 1 модель** +- После обработки — `del` + `gc.collect()` + `empty_cache()` +- FP16 по умолчанию (`torch_dtype=torch.float16`) + +### 4.2 Воспроизводимость + +```python +torch.manual_seed(42) +np.random.seed(42) +``` + +- Seed = 42 (фиксированный) в главном скрипте +- Deterministic DataLoader: `shuffle=False` для inference + +### 4.3 Атомарная запись файлов + +```python +import tempfile, os + +def atomic_save_npy(arr: np.ndarray, path: Path) -> None: + """Write .npy atomically via temp file + rename.""" + path.parent.mkdir(parents=True, exist_ok=True) + fd, tmp = tempfile.mkstemp(suffix=".npy.tmp", dir=path.parent) + os.close(fd) + try: + np.save(tmp, arr) + os.replace(tmp, path) + except BaseException: + if os.path.exists(tmp): + os.remove(tmp) + raise +``` + +- Все .npy/.json сохраняются через temp → `os.replace()` +- Позволяет безопасный `--resume` после сбоя + +### 4.4 Inference-декоратор + +```python +@torch.inference_mode() +def infer_batch(model: nn.Module, images: torch.Tensor, device: torch.device) -> torch.Tensor: + ... +``` + +- Всегда `@torch.inference_mode()` (не `torch.no_grad()`) +- Результат возвращать на CPU: `.cpu()` + +## 5. Структура модулей + +``` +project/ +├── in/config_files/ # .gin файлы (1 файл = 1 конфиг-класс) +├── src/ +│ ├── conf/ # Конфиг-классы + get_*_cfg() загрузчики +│ │ ├── pipeline_conf.py +│ │ ├── hardware_conf.py +│ │ ├── models_conf.py +│ │ └── ... +│ ├── augmentor/ # Бизнес-логика (dataset, models, inference, io_utils) +│ └── main.py # Точка входа: load configs → run pipeline +``` + +- **Разделение:** conf / dataset / models / inference / io_utils +- **Запрещено** смешивать логику конфигурации и инференса в одном файле + +## 6. При ревью кода — чеклист + +При `/code-style review`: + +- [ ] `from __future__ import annotations` первой строкой? +- [ ] Все функции/методы имеют type hints? +- [ ] Google-style docstrings на публичных классах/функциях? +- [ ] `@gin.configurable` только на классах? +- [ ] Нет `dataclass` + gin? +- [ ] Нет `argparse`? +- [ ] Нет захардкоженных model ID / промптов / размеров? +- [ ] Модели выгружаются после использования? +- [ ] Файлы сохраняются атомарно (temp + replace)? +- [ ] Seed установлен? +- [ ] `@torch.inference_mode()` на inference-функциях? +- [ ] Код и комментарии на английском? + +Подробные примеры: [reference/gin_examples.md](reference/gin_examples.md) diff --git a/code-style/reference/gin_examples.md b/code-style/reference/gin_examples.md new file mode 100644 index 0000000..8e69912 --- /dev/null +++ b/code-style/reference/gin_examples.md @@ -0,0 +1,280 @@ +# Gin-Config Strict Pattern: Reference Examples + +## Example 1: Config class + loader + .gin file + +### `src/conf/pipeline_conf.py` +```python +from __future__ import annotations + +import gin + + +@gin.configurable +class PipelineConfig: + """Configuration for the augmentation pipeline stages and output.""" + + def __init__( + self, + input_root: str = "/data/UAV-GeoLoc", + output_root: str = "/data/UAV-GeoLoc-aug", + stages: list[str] | None = None, + save_npy: bool = True, + save_vis: bool = True, + save_concat: bool = False, + resume: bool = True, + subset: str | None = None, + source: str | None = None, + log_level: str = "INFO", + ) -> None: + self.input_root = input_root + self.output_root = output_root + self.stages = stages or ["depth", "edges", "segmentation"] + self.save_npy = save_npy + self.save_vis = save_vis + self.save_concat = save_concat + self.resume = resume + self.subset = subset + self.source = source + self.log_level = log_level + + +def get_pipeline_cfg(path2cfg: str) -> PipelineConfig: + """Load pipeline config from gin file. + + Args: + path2cfg: Path to config directory (with trailing slash). + + Returns: + Instantiated PipelineConfig with values from gin file. + """ + gin.parse_config_file(f"{path2cfg}pipeline.gin") + return PipelineConfig() +``` + +### `in/config_files/pipeline.gin` +```gin +# Pipeline configuration +PipelineConfig.input_root = '/data/UAV-GeoLoc' +PipelineConfig.output_root = '/data/UAV-GeoLoc-aug' +PipelineConfig.stages = ['depth', 'edges', 'segmentation'] +PipelineConfig.save_npy = True +PipelineConfig.save_vis = True +PipelineConfig.save_concat = False +PipelineConfig.resume = True +PipelineConfig.subset = 'Rot' +PipelineConfig.source = None +PipelineConfig.log_level = 'INFO' +``` + +## Example 2: Model config with fallback IDs + +### `src/conf/models_conf.py` +```python +from __future__ import annotations + +import gin + + +@gin.configurable +class ModelsConfig: + """Model identifiers and fallback strategy.""" + + def __init__( + self, + depth_model_id: str = "depth-anything/DA3-BASE", + depth_fallback_id: str = "depth-anything/Depth-Anything-V2-Large-hf", + seg_model_type: str = "segearth-ov3", + seg_fallback_type: str = "segformer-b5", + seg_fallback_id: str = "nvidia/segformer-b5-finetuned-ade-640-640", + ) -> None: + self.depth_model_id = depth_model_id + self.depth_fallback_id = depth_fallback_id + self.seg_model_type = seg_model_type + self.seg_fallback_type = seg_fallback_type + self.seg_fallback_id = seg_fallback_id + + +def get_models_cfg(path2cfg: str) -> ModelsConfig: + """Load models config from gin file.""" + gin.parse_config_file(f"{path2cfg}models.gin") + return ModelsConfig() +``` + +## Example 3: Central config loader (`src/conf/config_loader.py`) + +```python +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +import gin + +from conf.pipeline_conf import PipelineConfig +from conf.hardware_conf import HardwareConfig +from conf.models_conf import ModelsConfig +from conf.input_conf import InputConfig +from conf.seg_conf import SegConfig + +logger = logging.getLogger(__name__) + + +def load_all_configs(path2cfg: str) -> dict[str, Any]: + """Parse ALL .gin files at once and return all config objects. + + CRITICAL: calls gin.clear_config() to reset global state. + + Args: + path2cfg: Path to config directory (WITH trailing slash). + + Returns: + Dict with config objects keyed by name. + """ + cfg_dir = Path(path2cfg) + gin_files = sorted(cfg_dir.glob("*.gin")) + + gin.clear_config() # MUST reset before loading + gin.parse_config_files_and_bindings( + config_files=[str(f) for f in gin_files], + bindings=[], + ) + + return { + "pipeline": PipelineConfig(), + "hardware": HardwareConfig(), + "models": ModelsConfig(), + "input": InputConfig(), + "seg": SegConfig(), + } + + +# Individual loaders — TESTING ONLY (always clear_config first): +def get_hardware_cfg(path2cfg: str) -> HardwareConfig: + gin.clear_config() + gin.parse_config_file(f"{path2cfg}hardware.gin") + return HardwareConfig() +``` + +## Example 4: Main entry point (uses load_all_configs) + +### `src/main.py` +```python +from __future__ import annotations + +import logging + +import numpy as np +import torch + +from conf.config_loader import load_all_configs +from utils.utils_file_dir import get_proj_dir + +logger = logging.getLogger(__name__) + + +def main() -> None: + """Entry point: load ALL configs at once, then run pipeline.""" + proj_dir = get_proj_dir() + path2cfg = f"{proj_dir}in/config_files/" + + # ONE call loads everything: + configs = load_all_configs(path2cfg) + + # Set seeds: + torch.manual_seed(42) + np.random.seed(42) + + # Pass configs explicitly: + run_pipeline( + configs["pipeline"], + configs["hardware"], + configs["models"], + configs["input"], + configs["seg"], + ) + + +if __name__ == "__main__": + main() +``` + +## Example 4: Model loading with config object + +```python +from __future__ import annotations + +import gc +import logging +from typing import Any + +import torch +import torch.nn as nn + +logger = logging.getLogger(__name__) + + +def load_depth_model( + models_conf: ModelsConfig, + hardware_conf: HardwareConfig, + device: torch.device, +) -> nn.Module: + """Load depth estimation model based on config. + + Args: + models_conf: Model IDs from gin config. + hardware_conf: FP16 and device settings. + device: Target CUDA device. + + Returns: + Loaded depth model on device. + """ + model_id = models_conf.depth_model_id + logger.info("Loading depth: %s", model_id) + + try: + from depth_anything_3 import DepthAnything3 + model = DepthAnything3.from_pretrained(model_id) + if hardware_conf.use_fp16: + model = model.half() + return model.to(device).eval() + except ImportError: + logger.warning("DA3 not found, falling back to %s", models_conf.depth_fallback_id) + from transformers import AutoModelForDepthEstimation + dtype = torch.float16 if hardware_conf.use_fp16 else torch.float32 + model = AutoModelForDepthEstimation.from_pretrained( + models_conf.depth_fallback_id, torch_dtype=dtype, + ) + return model.to(device).eval() + + +def unload_model(model: Any) -> None: + """Free GPU memory after model use.""" + del model + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() +``` + +## Example 5: Model loading with config objects (not hardcoded IDs) + +```python +# BAD: dataclass + gin +@gin.configurable +@dataclass # ← FORBIDDEN +class Config: + param: int = 1 + +# BAD: argparse +parser = argparse.ArgumentParser() # ← FORBIDDEN, use gin + +# BAD: global gin state inside function +def process(): + val = gin.query_parameter("Config.param") # ← FORBIDDEN + +# BAD: gin.constant / macros +LEARNING_RATE = gin.constant("lr", 0.001) # ← FORBIDDEN + +# BAD: hardcoded model ID +model = AutoModel.from_pretrained("depth-anything/DA3-BASE") # ← move to gin +```