Add code-style skill (gin-config strict pattern)
- code-style/SKILL.md — skill spec for the LISAD code-style enforcement - code-style/reference/gin_examples.md — gin-config canonical examples Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
220
code-style/SKILL.md
Normal file
220
code-style/SKILL.md
Normal file
@@ -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 <path>` — написать новый модуль по стандарту
|
||||||
|
- `/code-style review <path>` — проверить существующий код на соответствие
|
||||||
|
- `/code-style refactor <path>` — привести существующий код к стандарту
|
||||||
|
|
||||||
|
## 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_<name>_cfg(path2cfg: str) -> <ConfigClass>`
|
||||||
|
- Принимает путь к директории с конфигами (со слешем)
|
||||||
|
- Вызывает `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)
|
||||||
280
code-style/reference/gin_examples.md
Normal file
280
code-style/reference/gin_examples.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user