FALCON / config.py
JonathanJMK's picture
Upload complete FALCON model and project documentation
c645013 verified
Raw History Blame Contribute Delete
14.6 kB
from __future__ import annotations
from collections.abc import Mapping
from copy import deepcopy
from pathlib import Path
from typing import Any
import yaml
from .capabilities import SSA_ABLATIONS
COMPONENT_ORDER = ("detonator", "explosive", "battery")
LINK_ORDER = (
("battery", "detonator"),
("battery", "explosive"),
("detonator", "explosive"),
)
DETECTOR_VARIANTS = {
"seg-nano",
"seg-small",
"seg-medium",
"seg-large",
"seg-xlarge",
"seg-2xlarge",
}
DEFAULT_CONFIG: dict[str, Any] = {
"experiment": {
"name": "falcon-x",
"protocol": "dataset-v1",
"backbone_layout": "independent",
"world_size": None,
"ssa_ablation": "none",
},
"model": {
"vision_model": "facebook/dinov2-large",
"language_model": "lmsys/vicuna-7b-v1.5",
"image_size": 448,
"patch_size": 14,
"region_dim": 1024,
"roi_size": 4,
"max_regions": 100,
"component_order": list(COMPONENT_ORDER),
"link_order": [list(pair) for pair in LINK_ORDER],
"lora_rank": 16,
"lora_alpha": 32,
"lora_dropout": 0.05,
},
"detector": {
"variant": "seg-2xlarge",
"resolution": None,
"score_threshold": 0.15,
"nms_threshold": 0.6,
"mask_threshold": 0.5,
"max_regions": 100,
},
"training": {
"epochs": 1,
"batch_size": 1,
"gradient_accumulation": 16,
"learning_rate": 1e-4,
"weight_decay": 0.0,
"warmup_ratio": 0.03,
"max_text_tokens": 256,
"text_overflow_policy": "error",
"risk_loss_weight": 1.0,
"presence_loss_weight": 0.5,
"link_loss_weight": 0.5,
"seed": 42,
"precision": "bf16",
"tf32": True,
"sampling": "all",
"epoch_size": None,
},
}
# Per-device batches and gradient accumulation are configured separately.
DEFAULT_STAGES: dict[str, dict[str, Any]] = {
"stage1": {
"epochs": 12,
"batch_size": 1,
"gradient_accumulation": 16,
"learning_rate": 1e-4,
"encoder_learning_rate": 1.5e-4,
"weight_decay": 1e-4,
"lr_scheduler": "cosine",
"warmup_epochs": 0.0,
"multi_scale": False,
"expanded_scales": False,
"precision": "backend_mixed",
"tf32": True,
},
"stage2": deepcopy(DEFAULT_CONFIG["training"]),
"stage3": {
**deepcopy(DEFAULT_CONFIG["training"]),
"learning_rate": 1e-5,
"precision": "bf16",
},
}
DEFAULT_CONFIG["stages"] = deepcopy(DEFAULT_STAGES)
def _merge(
base: dict[str, Any], update: dict[str, Any], prefix: str = ""
) -> dict[str, Any]:
if not isinstance(update, dict):
raise ValueError(f"{prefix or 'Configuration'} must be a mapping")
for key, value in update.items():
name = f"{prefix}.{key}" if prefix else key
if key not in base:
raise ValueError(f"Unknown configuration key: {name}")
if isinstance(value, dict) and isinstance(base.get(key), dict):
_merge(base[key], value, name)
else:
base[key] = value
return base
def _positive_integer(value: Any, name: str) -> None:
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
raise ValueError(f"{name} must be a positive integer")
def _finite_number(value: Any, name: str, *, minimum: float = 0.0) -> float:
if isinstance(value, bool) or not isinstance(value, int | float):
raise ValueError(f"{name} must be a number")
number = float(value)
if not minimum <= number < float("inf"):
raise ValueError(f"{name} must be at least {minimum}")
return number
def validate_config(config: dict[str, Any]) -> None:
"""Reject architecture drift and common experiment-configuration mistakes."""
for section in ("model", "detector", "training"):
if not isinstance(config.get(section), dict):
raise ValueError(f"{section} must be a mapping")
model = config["model"]
detector = config["detector"]
experiment = config.get("experiment", DEFAULT_CONFIG["experiment"])
if not isinstance(experiment, dict):
raise ValueError("experiment must be a mapping")
if not isinstance(experiment["name"], str) or not experiment["name"].strip():
raise ValueError("experiment.name must be a non-empty string")
if experiment["protocol"] not in ("dataset-v1", "paper-v2"):
raise ValueError("experiment.protocol must be dataset-v1 or paper-v2")
if experiment["backbone_layout"] != "independent":
raise ValueError("Only the independent-backbone implementation is available")
if experiment.get("ssa_ablation", "none") not in SSA_ABLATIONS:
raise ValueError(f"experiment.ssa_ablation must be one of {SSA_ABLATIONS}")
if experiment["world_size"] is not None:
_positive_integer(experiment["world_size"], "experiment.world_size")
for key in ("vision_model", "language_model"):
if not isinstance(model[key], str) or not model[key].strip():
raise ValueError(
f"model.{key} must be a non-empty path or model identifier"
)
for key in ("image_size", "patch_size", "region_dim", "roi_size", "max_regions"):
_positive_integer(model[key], f"model.{key}")
if model["image_size"] % (2 * model["patch_size"]):
raise ValueError("model.image_size must be divisible by twice model.patch_size")
if tuple(model["component_order"]) != COMPONENT_ORDER:
raise ValueError(f"model.component_order must be {list(COMPONENT_ORDER)!r}")
if tuple(tuple(pair) for pair in model["link_order"]) != LINK_ORDER:
raise ValueError(
f"model.link_order must be {[list(pair) for pair in LINK_ORDER]!r}"
)
_positive_integer(model["lora_rank"], "model.lora_rank")
_positive_integer(model["lora_alpha"], "model.lora_alpha")
dropout = _finite_number(model["lora_dropout"], "model.lora_dropout")
if dropout >= 1.0:
raise ValueError("model.lora_dropout must be smaller than 1")
if detector["variant"] not in DETECTOR_VARIANTS:
raise ValueError(
f"detector.variant must be one of {sorted(DETECTOR_VARIANTS)!r}"
)
for key in ("score_threshold", "nms_threshold", "mask_threshold"):
if key not in detector:
continue
value = _finite_number(detector[key], f"detector.{key}")
if value > 1.0:
raise ValueError(f"detector.{key} must not exceed 1")
_positive_integer(detector["max_regions"], "detector.max_regions")
if detector["max_regions"] > model["max_regions"]:
raise ValueError("detector.max_regions must not exceed model.max_regions")
resolution = detector.get("resolution")
if resolution is not None:
_positive_integer(resolution, "detector.resolution")
# Pinned segmentation variants use patch size 12. Nano uses one local
# window; the other supported variants use two. No model load is needed.
divisor = 12 if detector["variant"] == "seg-nano" else 24
if resolution % divisor:
raise ValueError(
f"detector.resolution must be divisible by {divisor} for pinned RF-DETR"
)
_validate_training(config["training"], "training")
stages = config.get("stages", {})
if not isinstance(stages, dict):
raise ValueError("stages must be a mapping")
for stage, values in stages.items():
if stage not in DEFAULT_STAGES:
raise ValueError(f"Unknown training stage: {stage}")
if not isinstance(values, dict):
raise ValueError(f"stages.{stage} must be a mapping")
if stage == "stage1":
for key in ("epochs", "batch_size", "gradient_accumulation"):
_positive_integer(values[key], f"stages.stage1.{key}")
for key in (
"learning_rate",
"encoder_learning_rate",
"weight_decay",
"warmup_epochs",
):
_finite_number(values[key], f"stages.stage1.{key}")
if values["learning_rate"] == 0 or values["encoder_learning_rate"] == 0:
raise ValueError("Stage 1 learning rates must be positive")
if values["lr_scheduler"] not in ("cosine", "step"):
raise ValueError("stages.stage1.lr_scheduler must be cosine or step")
for key in ("multi_scale", "expanded_scales"):
if not isinstance(values[key], bool):
raise ValueError(f"stages.stage1.{key} must be boolean")
if values.get("precision") not in ("backend_mixed", "fp32"):
raise ValueError(
"stages.stage1.precision must be backend_mixed or fp32"
)
if not isinstance(values.get("tf32", True), bool):
raise ValueError("stages.stage1.tf32 must be boolean")
else:
_validate_training(values, f"stages.{stage}")
def _validate_precision(values: dict[str, Any], prefix: str) -> None:
if values.get("precision", "bf16") not in ("fp16", "bf16"):
raise ValueError(f"{prefix}.precision must be fp16 or bf16")
if not isinstance(values.get("tf32", True), bool):
raise ValueError(f"{prefix}.tf32 must be boolean")
def _validate_training(training: dict[str, Any], prefix: str) -> None:
for key in ("epochs", "batch_size", "gradient_accumulation", "max_text_tokens"):
_positive_integer(training[key], f"{prefix}.{key}")
if training["max_text_tokens"] < 3:
raise ValueError("training.max_text_tokens must be at least 3")
for key in (
"learning_rate",
"weight_decay",
"risk_loss_weight",
"presence_loss_weight",
"link_loss_weight",
):
_finite_number(training[key], f"{prefix}.{key}")
if training["learning_rate"] == 0:
raise ValueError("training.learning_rate must be greater than 0")
warmup = _finite_number(training["warmup_ratio"], "training.warmup_ratio")
if warmup >= 1.0:
raise ValueError("training.warmup_ratio must be smaller than 1")
if isinstance(training["seed"], bool) or not isinstance(training["seed"], int):
raise ValueError("training.seed must be an integer")
_validate_precision(training, prefix)
if training.get("text_overflow_policy", "error") not in ("error", "truncate"):
raise ValueError(f"{prefix}.text_overflow_policy must be error or truncate")
if training.get("sampling", "all") not in ("all", "family_balanced"):
raise ValueError(f"{prefix}.sampling must be all or family_balanced")
if training.get("epoch_size") is not None:
_positive_integer(training["epoch_size"], f"{prefix}.epoch_size")
def load_config(path: str | Path | None = None) -> dict[str, Any]:
config = deepcopy(DEFAULT_CONFIG)
payload: dict[str, Any] = {}
if path is not None:
with Path(path).expanduser().open(encoding="utf-8") as handle:
payload = yaml.safe_load(handle)
if payload is None:
payload = {}
if not isinstance(payload, dict):
raise ValueError("Configuration root must be a mapping")
_merge(config, payload)
# Explicit legacy/common training options still apply to both multimodal
# stages. Explicit per-stage overrides win. Defaults, however, differ between
# Stage 2 and Stage 3; a shared default LR must not erase that distinction.
common = payload.get("training", {})
stages = payload.get("stages", {})
if not isinstance(common, dict) or not isinstance(stages, dict):
raise ValueError("training and stages must be mappings")
for key in ("stage2", "stage3"):
merged = _merge(deepcopy(DEFAULT_STAGES[key]), common, "training")
config["stages"][key] = _merge(merged, stages.get(key, {}), f"stages.{key}")
validate_config(config)
return config
def resolve_stage_config(config: dict[str, Any], stage: int) -> dict[str, Any]:
"""Return a detached, fully resolved stage configuration.
The fallback preserves callers supplying the original configuration shape.
Stage-specific configuration takes precedence in newly loaded files.
"""
if stage not in (1, 2, 3):
raise ValueError("Training stage must be 1, 2, or 3")
key = f"stage{stage}"
if key in config.get("stages", {}):
return deepcopy(config["stages"][key])
if stage == 1:
return deepcopy(DEFAULT_STAGES[key])
return deepcopy(config["training"])
def apply_stage_overrides(
config: Mapping[str, Any],
stage: int,
overrides: Mapping[str, Any],
) -> dict[str, Any]:
"""Apply stage overrides to both runtime and saved checkpoint configuration."""
if not isinstance(config, Mapping):
raise TypeError("config must be a mapping")
if stage not in (1, 2, 3):
raise ValueError("Training stage must be 1, 2, or 3")
if not isinstance(overrides, Mapping):
raise TypeError("stage overrides must be a mapping")
effective = deepcopy(dict(config))
validate_config(effective)
resolved = resolve_stage_config(effective, stage)
unknown = sorted(set(overrides).difference(resolved))
if unknown:
prefix = f"stages.stage{stage}"
raise ValueError(
"Unknown stage override"
+ ("s" if len(unknown) != 1 else "")
+ ": "
+ ", ".join(f"{prefix}.{name}" for name in unknown)
)
resolved.update(deepcopy(dict(overrides)))
effective.setdefault("stages", {})[f"stage{stage}"] = resolved
validate_config(effective)
return effective
def paper_reproduction_issues(config: dict[str, Any]) -> list[str]:
"""Report known eligibility blockers, not a claim of historical reproduction."""
issues = [
"Binary presence supervision uses BCE-with-logits instead of the paper's L1 loss."
]
if (
config.get("experiment", {}).get("backbone_layout", "independent")
== "independent"
):
issues.append(
"The shared-feature detector described in the paper is not implemented; "
"the independent RF-DETR/DINO topology is a documented deviation."
)
if config["detector"].get("resolution") != 448:
issues.append(
"The detector does not use the paper's stated 448-pixel resolution; "
"448 is incompatible with the pinned segmentation backend."
)
return issues